Compare commits

..

3 Commits

Author SHA1 Message Date
shenlong-tanwen
023d2195f9 feat: inline storage columns in remote and local tables 2025-07-11 01:37:02 +05:30
mertalev
c482bdfae7 thumbhash render box 2025-07-10 16:41:53 +03:00
mertalev
0dadfc52dd thumbhash improvements 2025-07-08 16:20:11 +03:00
545 changed files with 9720 additions and 21740 deletions

View File

@@ -74,7 +74,7 @@ install_dependencies() {
( (
cd "${IMMICH_WORKSPACE}" || exit 1 cd "${IMMICH_WORKSPACE}" || exit 1
export CI=1 FROZEN=1 OFFLINE=1 export CI=1 FROZEN=1 OFFLINE=1
run_cmd make setup-web-dev setup-server-dev run_cmd make setup-dev
) )
log "" log ""
} }

View File

@@ -122,17 +122,17 @@ jobs:
IS_MAIN: ${{ github.ref == 'refs/heads/main' }} IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
run: | run: |
if [[ $IS_MAIN == 'true' ]]; then if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release --flavor production flutter build apk --release
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64 flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else else
flutter build apk --debug --flavor production --split-per-abi --target-platform android-arm64 flutter build apk --debug --split-per-abi --target-platform android-arm64
fi fi
- name: Publish Android Artifact - name: Publish Android Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-apk-signed name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/**/*.apk path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Save Gradle Cache - name: Save Gradle Cache
id: cache-gradle-save id: cache-gradle-save

View File

@@ -131,7 +131,7 @@ jobs:
tag-suffix: '-rocm' tag-suffix: '-rocm'
platforms: linux/amd64 platforms: linux/amd64
runner-mapping: '{"linux/amd64": "mich"}' runner-mapping: '{"linux/amd64": "mich"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@@ -154,7 +154,7 @@ jobs:
name: Build and Push Server name: Build and Push Server
needs: pre-job needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
permissions: permissions:
contents: read contents: read
actions: read actions: read

View File

@@ -1,13 +0,0 @@
name: Org Checks
on:
pull_request_review:
pull_request:
jobs:
check-approvals:
name: Check for Team/Admin Review
uses: immich-app/devtools/.github/workflows/required-approval.yml@main
permissions:
pull-requests: read
contents: read

View File

@@ -61,7 +61,8 @@ jobs:
run: dart pub get run: dart pub get
- name: Install DCM - name: Install DCM
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 # TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
uses: bo0tzz/setup-dcm@b4952ab813659c03513b57bd78bfe3f634171f8a
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
version: auto version: auto

19
.vscode/launch.json vendored
View File

@@ -18,25 +18,6 @@
"name": "Immich Workers", "name": "Immich Workers",
"remoteRoot": "/usr/src/app", "remoteRoot": "/usr/src/app",
"localRoot": "${workspaceFolder}/server" "localRoot": "${workspaceFolder}/server"
},
{
"name": "Flavor - Production",
"request": "launch",
"type": "dart",
"codeLens": {
"for": [
"run-test",
"run-test-file",
"run-file",
"debug-test",
"debug-test-file",
"debug-file",
],
"title": "${debugType}",
},
"args": [
"--flavor", "production"
],
} }
] ]
} }

View File

@@ -106,5 +106,4 @@ clean:
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
setup-server-dev: install-server setup-dev: install-server install-sdk build-sdk install-web
setup-web-dev: install-sdk build-sdk install-web

162
cli/package-lock.json generated
View File

@@ -42,7 +42,7 @@
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",
"vite": "^7.0.0", "vite": "^6.0.0",
"vite-tsconfig-paths": "^5.0.0", "vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0", "vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0", "vitest-fetch-mock": "^0.4.0",
@@ -1365,17 +1365,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/type-utils": "8.36.0", "@typescript-eslint/type-utils": "8.35.1",
"@typescript-eslint/utils": "8.36.0", "@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.35.1",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1389,7 +1389,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.36.0", "@typescript-eslint/parser": "^8.35.1",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
@@ -1405,16 +1405,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1430,14 +1430,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/tsconfig-utils": "^8.35.1",
"@typescript-eslint/types": "^8.36.0", "@typescript-eslint/types": "^8.35.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1452,14 +1452,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0" "@typescript-eslint/visitor-keys": "8.35.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1470,9 +1470,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1487,14 +1487,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/utils": "8.36.0", "@typescript-eslint/utils": "8.35.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -1511,9 +1511,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1525,16 +1525,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/project-service": "8.35.1",
"@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.35.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1580,16 +1580,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.36.0" "@typescript-eslint/typescript-estree": "8.35.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1604,13 +1604,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -3466,9 +3466,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3486,7 +3486,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -4125,15 +4125,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==", "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.36.0", "@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/utils": "8.36.0" "@typescript-eslint/utils": "8.35.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4196,24 +4196,24 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.0.4", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.4",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"postcss": "^8.5.6", "postcss": "^8.5.3",
"rollup": "^4.40.0", "rollup": "^4.34.9",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
@@ -4222,14 +4222,14 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "^1.70.0", "sass": "*",
"sass-embedded": "^1.70.0", "sass-embedded": "*",
"stylus": ">=0.54.8", "stylus": "*",
"sugarss": "^5.0.0", "sugarss": "*",
"terser": "^5.16.0", "terser": "^5.16.0",
"tsx": "^4.8.1", "tsx": "^4.8.1",
"yaml": "^2.4.2" "yaml": "^2.4.2"
@@ -4314,9 +4314,9 @@
} }
}, },
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.4.6", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {

View File

@@ -36,7 +36,7 @@
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",
"vite": "^7.0.0", "vite": "^6.0.0",
"vite-tsconfig-paths": "^5.0.0", "vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0", "vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0", "vitest-fetch-mock": "^0.4.0",

View File

@@ -116,7 +116,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1

View File

@@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -83,7 +83,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996 image: prom/prometheus@sha256:7a34573f0b9c952286b33d537f233cd5b708e12263733aa646e50c33f598f16c
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus

View File

@@ -49,7 +49,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11 image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -46,12 +46,6 @@ services:
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page. When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" /> <img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />
Additionally, some jobs (such as memories generation) run on a schedule, which is every night at midnight by default. To change when they run or enable/disable a job navigate to System Settings -> [Nightly Tasks Settings](https://my.immich.app/admin/system-settings?isOpen=nightly-tasks).
<img src={require('./img/admin-nightly-tasks.webp').default} width="60%" title="Admin nightly tasks" />
:::note
Some jobs ([External Libraries](/docs/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings.
:::

View File

@@ -2,7 +2,7 @@
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template. Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders) You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
## Enable folder view ## Enable folder view

View File

@@ -56,7 +56,7 @@ Internally, Immich uses the [glob](https://www.npmjs.com/package/glob) package t
### Automatic watching (EXPERIMENTAL) ### Automatic watching (EXPERIMENTAL)
This feature is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes. If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
@@ -112,7 +112,7 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
These actions must be performed by the Immich administrator. These actions must be performed by the Immich administrator.
- Click on your avatar in the upper right corner - Click on your avatar on the upper right corner
- Click on Administration -> External Libraries - Click on Administration -> External Libraries
- Click on Create an external library… - Click on Create an external library…
- Select which user owns the library, this can not be changed later - Select which user owns the library, this can not be changed later
@@ -159,7 +159,9 @@ Within seconds, the assets from the old-pics and videos folders should show up i
Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template. Folder view provides an additional view besides the timeline that is similar to a file explorer. It allows you to navigate through the folders and files in the library. This feature is handy for a highly curated and customized external library or a nicely configured storage template.
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders) You can enable this feature under [`Account Settings > Features > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
The UI is currently only available for the web; mobile will come in a subsequent release.
<img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' /> <img src={require('./img/folder-view-1.webp').default} width="100%" title='Folder-view' />
@@ -169,7 +171,7 @@ You can enable this feature under [`Account Settings > Features > Folders`](http
Only an admin can do this. Only an admin can do this.
::: :::
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External Library. You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/). You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
<img src={require('./img/library-custom-scan-interval.webp').default} width="75%" title='Set custom scan interval for external library' /> <img src={require('./img/library-custom-scan-interval.webp').default} width="75%" title='Set custom scan interval for external library' />

View File

@@ -41,7 +41,7 @@ In the Immich web UI:
- Click Add path - Click Add path
<img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" /> <img src={require('./img/add-path-button.webp').default} width="50%" title="Add Path button" />
- Enter **/home/user/photos1** as the path and click Add - Enter **/usr/src/app/external** as the path and click Add
<img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" /> <img src={require('./img/add-path-field.webp').default} width="50%" title="Add Path field" />
- Save the new path - Save the new path

View File

@@ -85,7 +85,6 @@ import React from 'react';
import { Item, Timeline } from '../components/timeline'; import { Item, Timeline } from '../components/timeline';
const releases = { const releases = {
'v1.135.0': new Date(2025, 5, 18),
'v1.133.0': new Date(2025, 4, 21), 'v1.133.0': new Date(2025, 4, 21),
'v1.130.0': new Date(2025, 2, 25), 'v1.130.0': new Date(2025, 2, 25),
'v1.127.0': new Date(2025, 1, 26), 'v1.127.0': new Date(2025, 1, 26),
@@ -197,6 +196,14 @@ const roadmap: Item[] = [
description: 'Automate tasks with workflows', description: 'Automate tasks with workflows',
getDateLabel: () => 'Planned for 2025', getDateLabel: () => 'Planned for 2025',
}, },
{
done: false,
icon: mdiTableKey,
iconColor: 'gray',
title: 'Fine grained access controls',
description: 'Granular access controls for users and api keys',
getDateLabel: () => 'Planned for 2025',
},
{ {
done: false, done: false,
icon: mdiImageEdit, icon: mdiImageEdit,
@@ -232,26 +239,12 @@ const roadmap: Item[] = [
]; ];
const milestones: Item[] = [ const milestones: Item[] = [
{
icon: mdiStar,
iconColor: 'gold',
title: '70,000 Stars',
description: 'Reached 70K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2025, 6, 9)),
},
withRelease({
icon: mdiTableKey,
iconColor: 'gray',
title: 'Fine grained access controls',
description: 'Granular access controls for api keys',
release: 'v1.135.0',
}),
withRelease({ withRelease({
icon: mdiCast, icon: mdiCast,
iconColor: 'aqua', iconColor: 'aqua',
title: 'Google Cast (web and mobile)', title: 'Google Cast (web)',
description: 'Cast assets to Google Cast/Chromecast compatible devices', description: 'Cast assets to Google Cast/Chromecast compatible devices',
release: 'v1.135.0', release: 'v1.133.0',
}), }),
withRelease({ withRelease({
icon: mdiLockOutline, icon: mdiLockOutline,

View File

@@ -1,5 +1,4 @@
/docs /docs/overview/welcome 307 /docs /docs/overview/introduction 307
/docs/ /docs/overview/welcome 307
/docs/mobile-app-beta-program /docs/features/mobile-app 307 /docs/mobile-app-beta-program /docs/features/mobile-app 307
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307 /docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
/docs/install /docs/install/docker-compose 307 /docs/install /docs/install/docker-compose 307

141
e2e/package-lock.json generated
View File

@@ -14,7 +14,6 @@
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.15.33", "@types/node": "^22.15.33",
"@types/oidc-provider": "^9.0.0", "@types/oidc-provider": "^9.0.0",
@@ -83,7 +82,7 @@
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",
"vite": "^7.0.0", "vite": "^6.0.0",
"vite-tsconfig-paths": "^5.0.0", "vite-tsconfig-paths": "^5.0.0",
"vitest": "^3.0.0", "vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0", "vitest-fetch-mock": "^0.4.0",
@@ -2101,17 +2100,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/type-utils": "8.36.0", "@typescript-eslint/type-utils": "8.35.1",
"@typescript-eslint/utils": "8.36.0", "@typescript-eslint/utils": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.35.1",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -2125,7 +2124,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.36.0", "@typescript-eslint/parser": "^8.35.1",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
@@ -2141,16 +2140,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2166,14 +2165,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/tsconfig-utils": "^8.35.1",
"@typescript-eslint/types": "^8.36.0", "@typescript-eslint/types": "^8.35.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2188,14 +2187,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0" "@typescript-eslint/visitor-keys": "8.35.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2206,9 +2205,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2223,14 +2222,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/typescript-estree": "8.35.1",
"@typescript-eslint/utils": "8.36.0", "@typescript-eslint/utils": "8.35.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -2247,9 +2246,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2261,16 +2260,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/project-service": "8.35.1",
"@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.35.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/visitor-keys": "8.36.0", "@typescript-eslint/visitor-keys": "8.35.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -2316,16 +2315,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/scope-manager": "8.35.1",
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"@typescript-eslint/typescript-estree": "8.36.0" "@typescript-eslint/typescript-estree": "8.35.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2340,13 +2339,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.36.0", "@typescript-eslint/types": "8.35.1",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -4812,9 +4811,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.7.1", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -6430,9 +6429,9 @@
} }
}, },
"node_modules/superagent": { "node_modules/superagent": {
"version": "10.2.2", "version": "10.2.1",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
"integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==", "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -6451,14 +6450,14 @@
} }
}, },
"node_modules/supertest": { "node_modules/supertest": {
"version": "7.1.3", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
"integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==", "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"methods": "^1.1.2", "methods": "^1.1.2",
"superagent": "^10.2.2" "superagent": "^10.2.1"
}, },
"engines": { "engines": {
"node": ">=14.18.0" "node": ">=14.18.0"
@@ -6778,15 +6777,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.36.0", "version": "8.35.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==", "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.36.0", "@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/utils": "8.36.0" "@typescript-eslint/utils": "8.35.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -24,7 +24,6 @@
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.15.33", "@types/node": "^22.15.33",
"@types/oidc-provider": "^9.0.0", "@types/oidc-provider": "^9.0.0",

View File

@@ -7,7 +7,6 @@ import {
ReactionType, ReactionType,
createActivity as create, createActivity as create,
createAlbum, createAlbum,
removeAssetFromAlbum,
} from '@immich/sdk'; } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -343,36 +342,5 @@ describe('/activities', () => {
expect(status).toBe(204); expect(status).toBe(204);
}); });
it('should return empty list when asset is removed', async () => {
const album3 = await createAlbum(
{
createAlbumDto: {
albumName: 'Album 3',
assetIds: [asset.id],
},
},
{ headers: asBearerAuth(admin.accessToken) },
);
await createActivity({ albumId: album3.id, assetId: asset.id, type: ReactionType.Like });
await removeAssetFromAlbum(
{
id: album3.id,
bulkIdsDto: {
ids: [asset.id],
},
},
{ headers: asBearerAuth(admin.accessToken) },
);
const { status, body } = await request(app)
.get('/activities')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body).toEqual([]);
});
}); });
}); });

View File

@@ -20,7 +20,7 @@ describe('/api-keys', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await utils.resetDatabase(['api_key']); await utils.resetDatabase(['api_keys']);
}); });
describe('POST /api-keys', () => { describe('POST /api-keys', () => {

View File

@@ -15,6 +15,12 @@ describe('/system-config', () => {
}); });
describe('PUT /system-config', () => { describe('PUT /system-config', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/system-config');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should always return the new config', async () => { it('should always return the new config', async () => {
const config = await getSystemConfig(admin.accessToken); const config = await getSystemConfig(admin.accessToken);

View File

@@ -37,7 +37,7 @@ describe('/tags', () => {
beforeEach(async () => { beforeEach(async () => {
// tagging assets eventually triggers metadata extraction which can impact other tests // tagging assets eventually triggers metadata extraction which can impact other tests
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.resetDatabase(['tag']); await utils.resetDatabase(['tags']);
}); });
describe('POST /tags', () => { describe('POST /tags', () => {

View File

@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await utils.resetDatabase(['asset', 'album']); await utils.resetDatabase(['assets', 'albums']);
}); });
describe(`immich upload /path/to/file.jpg`, () => { describe(`immich upload /path/to/file.jpg`, () => {

View File

@@ -116,7 +116,6 @@ export const deviceDto = {
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String), updatedAt: expect.any(String),
current: true, current: true,
isPendingSyncReset: false,
deviceOS: '', deviceOS: '',
deviceType: '', deviceType: '',
}, },

View File

@@ -154,19 +154,19 @@ export const utils = {
tables = tables || [ tables = tables || [
// TODO e2e test for deleting a stack, since it is quite complex // TODO e2e test for deleting a stack, since it is quite complex
'stack', 'asset_stack',
'library', 'libraries',
'shared_link', 'shared_links',
'person', 'person',
'album', 'albums',
'asset', 'assets',
'asset_face', 'asset_faces',
'activity', 'activity',
'api_key', 'api_keys',
'session', 'sessions',
'user', 'users',
'system_metadata', 'system_metadata',
'tag', 'tags',
]; ];
const sql: string[] = []; const sql: string[] = [];
@@ -175,7 +175,7 @@ export const utils = {
if (table === 'system_metadata') { if (table === 'system_metadata') {
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`); sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
} else { } else {
sql.push(`DELETE FROM "${table}" CASCADE;`); sql.push(`DELETE FROM ${table} CASCADE;`);
} }
} }
@@ -451,7 +451,7 @@ export const utils = {
return; return;
} }
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
}, },
setPersonThumbnail: async (personId: string) => { setPersonThumbnail: async (personId: string) => {

View File

@@ -166,20 +166,6 @@
"metadata_settings_description": "Manage metadata settings", "metadata_settings_description": "Manage metadata settings",
"migration_job": "Migration", "migration_job": "Migration",
"migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure",
"nightly_tasks_cluster_faces_setting_description": "Run facial recognition on newly detected faces",
"nightly_tasks_cluster_new_faces_setting": "Cluster new faces",
"nightly_tasks_database_cleanup_setting": "Database cleanup tasks",
"nightly_tasks_database_cleanup_setting_description": "Clean up old, expired data from the database",
"nightly_tasks_generate_memories_setting": "Generate memories",
"nightly_tasks_generate_memories_setting_description": "Create new memories from assets",
"nightly_tasks_missing_thumbnails_setting": "Generate missing thumbnails",
"nightly_tasks_missing_thumbnails_setting_description": "Queue assets without thumbnails for thumbnail generation",
"nightly_tasks_settings": "Nightly Tasks Settings",
"nightly_tasks_settings_description": "Manage nightly tasks",
"nightly_tasks_start_time_setting": "Start time",
"nightly_tasks_start_time_setting_description": "The time at which the server starts running the nightly tasks",
"nightly_tasks_sync_quota_usage_setting": "Sync quota usage",
"nightly_tasks_sync_quota_usage_setting_description": "Update user storage quota, based on current usage",
"no_paths_added": "No paths added", "no_paths_added": "No paths added",
"no_pattern_added": "No pattern added", "no_pattern_added": "No pattern added",
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
@@ -749,7 +735,6 @@
"delete_key": "Delete key", "delete_key": "Delete key",
"delete_library": "Delete Library", "delete_library": "Delete Library",
"delete_link": "Delete link", "delete_link": "Delete link",
"delete_local_action_prompt": "{count} deleted locally",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway", "delete_local_dialog_ok_force": "Delete Anyway",
"delete_others": "Delete others", "delete_others": "Delete others",
@@ -1147,7 +1132,6 @@
"library_page_sort_created": "Created date", "library_page_sort_created": "Created date",
"library_page_sort_last_modified": "Last modified", "library_page_sort_last_modified": "Last modified",
"library_page_sort_title": "Album title", "library_page_sort_title": "Album title",
"licenses": "Licenses",
"light": "Light", "light": "Light",
"like_deleted": "Like deleted", "like_deleted": "Like deleted",
"link_motion_video": "Link motion video", "link_motion_video": "Link motion video",
@@ -1517,7 +1501,6 @@
"remove_custom_date_range": "Remove custom date range", "remove_custom_date_range": "Remove custom date range",
"remove_deleted_assets": "Remove Deleted Assets", "remove_deleted_assets": "Remove Deleted Assets",
"remove_from_album": "Remove from album", "remove_from_album": "Remove from album",
"remove_from_album_action_prompt": "{count} removed from the album",
"remove_from_favorites": "Remove from favorites", "remove_from_favorites": "Remove from favorites",
"remove_from_lock_folder_action_prompt": "{count} removed from the locked folder", "remove_from_lock_folder_action_prompt": "{count} removed from the locked folder",
"remove_from_locked_folder": "Remove from locked folder", "remove_from_locked_folder": "Remove from locked folder",
@@ -1903,7 +1886,6 @@
"unselect_all_in": "Unselect all in {group}", "unselect_all_in": "Unselect all in {group}",
"unstack": "Un-stack", "unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untagged": "Untagged",
"up_next": "Up next", "up_next": "Up next",
"updated_at": "Updated", "updated_at": "Updated",
"updated_password": "Updated password", "updated_password": "Updated password",
@@ -1940,7 +1922,6 @@
"user_usage_stats_description": "View account usage statistics", "user_usage_stats_description": "View account usage statistics",
"username": "Username", "username": "Username",
"users": "Users", "users": "Users",
"users_added_to_album_count": "Added {count, plural, one {# user} other {# users}} to the album",
"utilities": "Utilities", "utilities": "Utilities",
"validate": "Validate", "validate": "Validate",
"validate_endpoint_error": "Please enter a valid URL", "validate_endpoint_error": "Please enter a valid URL",

View File

@@ -4,12 +4,9 @@ import sys
import requests import requests
port = os.getenv("IMMICH_PORT", 3003) port = os.getenv("IMMICH_PORT", 3003)
host = os.getenv("IMMICH_HOST", "0.0.0.0")
host = "localhost" if host == "0.0.0.0" else host
try: try:
response = requests.get(f"http://{host}:{port}/ping", timeout=2) response = requests.get(f"http://localhost:{port}/ping", timeout=2)
if response.status_code == 200: if response.status_code == 200:
sys.exit(0) sys.exit(0)
sys.exit(1) sys.exit(1)

View File

@@ -1,3 +1,3 @@
{ {
"flutter": "3.32.6" "flutter": "3.29.3"
} }

View File

@@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.32.6", "dart.flutterSdkPath": ".fvm/versions/3.29.3",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },

View File

@@ -66,20 +66,6 @@ android {
} }
} }
flavorDimensions "default"
productFlavors {
production {
dimension "default"
applicationId "app.alextran.immich"
}
beta {
dimension "default"
applicationId "app.alextran.immich.beta"
versionNameSuffix "-BETA"
}
}
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:label="Immich Beta" tools:replace="android:label" />
</manifest>

View File

@@ -100,24 +100,24 @@
<!-- my.immich.app deep link --> <!-- my.immich.app deep link -->
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:path="/" /> android:path="/" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:pathPrefix="/albums/" /> android:pathPrefix="/albums/" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:pathPrefix="/memories/" /> android:pathPrefix="/memories/" />
<data <data
android:host="my.immich.app" android:host="my.immich.app"
android:pathPrefix="/photos/" /> android:pathPrefix="/photos/" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@@ -87,8 +87,7 @@ data class PlatformAsset (
val updatedAt: Long? = null, val updatedAt: Long? = null,
val width: Long? = null, val width: Long? = null,
val height: Long? = null, val height: Long? = null,
val durationInSeconds: Long, val durationInSeconds: Long
val orientation: Long
) )
{ {
companion object { companion object {
@@ -101,8 +100,7 @@ data class PlatformAsset (
val width = pigeonVar_list[5] as Long? val width = pigeonVar_list[5] as Long?
val height = pigeonVar_list[6] as Long? val height = pigeonVar_list[6] as Long?
val durationInSeconds = pigeonVar_list[7] as Long val durationInSeconds = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
@@ -115,7 +113,6 @@ data class PlatformAsset (
width, width,
height, height,
durationInSeconds, durationInSeconds,
orientation,
) )
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -40,8 +40,7 @@ open class NativeSyncApiImplBase(context: Context) {
MediaStore.MediaColumns.BUCKET_ID, MediaStore.MediaColumns.BUCKET_ID,
MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.HEIGHT,
MediaStore.MediaColumns.DURATION, MediaStore.MediaColumns.DURATION
MediaStore.MediaColumns.ORIENTATION,
) )
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
@@ -75,8 +74,6 @@ open class NativeSyncApiImplBase(context: Context) {
val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
val orientationColumn =
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
while (c.moveToNext()) { while (c.moveToNext()) {
val id = c.getLong(idColumn).toString() val id = c.getLong(idColumn).toString()
@@ -104,7 +101,6 @@ open class NativeSyncApiImplBase(context: Context) {
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
else c.getLong(durationColumn) / 1000 else c.getLong(durationColumn) / 1000
val bucketId = c.getString(bucketIdColumn) val bucketId = c.getString(bucketIdColumn)
val orientation = c.getInt(orientationColumn)
val asset = PlatformAsset( val asset = PlatformAsset(
id, id,
@@ -114,8 +110,7 @@ open class NativeSyncApiImplBase(context: Context) {
modifiedAt, modifiedAt,
width, width,
height, height,
duration, duration
orientation.toLong(),
) )
yield(AssetResult.ValidAsset(asset, bucketId)) yield(AssetResult.ValidAsset(asset, bucketId))
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,34 +5,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "82.0.0" version: "80.0.0"
analyzer: analyzer:
dependency: "direct main" dependency: "direct main"
description: description:
name: analyzer name: analyzer
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.4.5" version: "7.3.0"
analyzer_plugin: analyzer_plugin:
dependency: "direct main" dependency: "direct main"
description: description:
name: analyzer_plugin name: analyzer_plugin
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.1" version: "0.13.0"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" version: "2.6.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: checked_yaml name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.4" version: "2.0.3"
ci: ci:
dependency: transitive dependency: transitive
description: description:
@@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: custom_lint_visitor name: custom_lint_visitor
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+7.4.5" version: "1.0.0+7.3.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@@ -157,10 +157,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: freezed_annotation name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.0.0"
glob: glob:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -213,18 +213,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.16.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
name: package_config name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.1.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -317,10 +317,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.4"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -341,18 +341,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -362,4 +362,4 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.8.0 <4.0.0" dart: ">=3.8.0-0 <4.0.0"

View File

@@ -26,7 +26,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@@ -44,7 +43,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
disableMainThreadChecker = "YES" disableMainThreadChecker = "YES"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"

View File

@@ -138,7 +138,6 @@ struct PlatformAsset: Hashable {
var width: Int64? = nil var width: Int64? = nil
var height: Int64? = nil var height: Int64? = nil
var durationInSeconds: Int64 var durationInSeconds: Int64
var orientation: Int64
// swift-format-ignore: AlwaysUseLowerCamelCase // swift-format-ignore: AlwaysUseLowerCamelCase
@@ -151,7 +150,6 @@ struct PlatformAsset: Hashable {
let width: Int64? = nilOrValue(pigeonVar_list[5]) let width: Int64? = nilOrValue(pigeonVar_list[5])
let height: Int64? = nilOrValue(pigeonVar_list[6]) let height: Int64? = nilOrValue(pigeonVar_list[6])
let durationInSeconds = pigeonVar_list[7] as! Int64 let durationInSeconds = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64
return PlatformAsset( return PlatformAsset(
id: id, id: id,
@@ -161,8 +159,7 @@ struct PlatformAsset: Hashable {
updatedAt: updatedAt, updatedAt: updatedAt,
width: width, width: width,
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds
orientation: orientation
) )
} }
func toList() -> [Any?] { func toList() -> [Any?] {
@@ -175,7 +172,6 @@ struct PlatformAsset: Hashable {
width, width,
height, height,
durationInSeconds, durationInSeconds,
orientation,
] ]
} }
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {

View File

@@ -27,8 +27,7 @@ extension PHAsset {
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
width: Int64(pixelWidth), width: Int64(pixelWidth),
height: Int64(pixelHeight), height: Int64(pixelHeight),
durationInSeconds: Int64(duration), durationInSeconds: Int64(duration)
orientation: 0
) )
} }
} }
@@ -170,8 +169,7 @@ class NativeSyncApiImpl: NativeSyncApi {
id: asset.localIdentifier, id: asset.localIdentifier,
name: "", name: "",
type: 0, type: 0,
durationInSeconds: 0, durationInSeconds: 0
orientation: 0
) )
if (updatedAssets.contains(AssetWrapper(with: predicate))) { if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue continue

View File

@@ -0,0 +1,59 @@
import SwiftUI
import WidgetKit
func buildEntry(
api: ImmichAPI,
asset: Asset,
dateOffset: Int,
subtitle: String? = nil
)
async throws -> ImageEntry
{
let entryDate = Calendar.current.date(
byAdding: .minute,
value: dateOffset * 20,
to: Date.now
)!
let image = try await api.fetchImage(asset: asset)
return ImageEntry(date: entryDate, image: image, subtitle: subtitle, deepLink: asset.deepLink)
}
func generateRandomEntries(
api: ImmichAPI,
now: Date,
count: Int,
albumId: String? = nil,
subtitle: String? = nil
)
async throws -> [ImageEntry]
{
var entries: [ImageEntry] = []
let albumIds = albumId != nil ? [albumId!] : []
let randomAssets = try await api.fetchSearchResults(
with: SearchFilters(size: count, albumIds: albumIds)
)
await withTaskGroup(of: ImageEntry?.self) { group in
for (dateOffset, asset) in randomAssets.enumerated() {
group.addTask {
return try? await buildEntry(
api: api,
asset: asset,
dateOffset: dateOffset,
subtitle: subtitle
)
}
}
for await result in group {
if let entry = result {
entries.append(entry)
}
}
}
return entries
}

View File

@@ -1,148 +0,0 @@
import SwiftUI
import WidgetKit
typealias EntryMetadata = ImageEntry.Metadata
struct ImageEntry: TimelineEntry {
let date: Date
var image: UIImage?
var metadata: Metadata = Metadata()
struct Metadata: Codable {
var subtitle: String? = nil
var error: WidgetError? = nil
var deepLink: URL? = nil
}
static func build(
api: ImmichAPI,
asset: Asset,
dateOffset: Int,
subtitle: String? = nil
)
async throws -> Self
{
let entryDate = Calendar.current.date(
byAdding: .minute,
value: dateOffset * 20,
to: Date.now
)!
let image = try await api.fetchImage(asset: asset)
return Self(
date: entryDate,
image: image,
metadata: EntryMetadata(
subtitle: subtitle,
deepLink: asset.deepLink
)
)
}
func cache(for key: String) throws {
if let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
) {
let imageURL = containerURL.appendingPathComponent("\(key)_image.png")
let metadataURL = containerURL.appendingPathComponent(
"\(key)_metadata.json"
)
// build metadata JSON
let entryMetadata = try JSONEncoder().encode(self.metadata)
// write to disk
try self.image?.pngData()?.write(to: imageURL, options: .atomic)
try entryMetadata.write(to: metadataURL, options: .atomic)
}
}
static func loadCached(for key: String, at date: Date = Date.now)
-> ImageEntry?
{
if let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: IMMICH_SHARE_GROUP
) {
let imageURL = containerURL.appendingPathComponent("\(key)_image.png")
let metadataURL = containerURL.appendingPathComponent(
"\(key)_metadata.json"
)
guard let imageData = try? Data(contentsOf: imageURL),
let metadataJSON = try? Data(contentsOf: metadataURL),
let decodedMetadata = try? JSONDecoder().decode(
Metadata.self,
from: metadataJSON
)
else {
return nil
}
return ImageEntry(
date: date,
image: UIImage(data: imageData),
metadata: decodedMetadata
)
}
return nil
}
static func handleError(
for key: String,
error: WidgetError = .fetchFailed
) -> Timeline<ImageEntry> {
var timelineEntry = ImageEntry(
date: Date.now,
image: nil,
metadata: EntryMetadata(error: error)
)
// use cache if generic failed error
// we want to show the other errors to the user since without intervention,
// it will never succeed
if error == .fetchFailed, let cachedEntry = ImageEntry.loadCached(for: key)
{
timelineEntry = cachedEntry
}
return Timeline(entries: [timelineEntry], policy: .atEnd)
}
}
func generateRandomEntries(
api: ImmichAPI,
now: Date,
count: Int,
filter: SearchFilter = Album.NONE.filter,
subtitle: String? = nil
)
async throws -> [ImageEntry]
{
var entries: [ImageEntry] = []
let randomAssets = try await api.fetchSearchResults(with: filter)
await withTaskGroup(of: ImageEntry?.self) { group in
for (dateOffset, asset) in randomAssets.enumerated() {
group.addTask {
return try? await ImageEntry.build(
api: api,
asset: asset,
dateOffset: dateOffset,
subtitle: subtitle
)
}
}
for await result in group {
if let entry = result {
entries.append(entry)
}
}
}
return entries
}

View File

@@ -1,14 +1,23 @@
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
extension Image { struct ImageEntry: TimelineEntry {
@ViewBuilder let date: Date
func tintedWidgetImageModifier() -> some View { var image: UIImage?
if #available(iOS 18.0, *) { var subtitle: String? = nil
self var error: WidgetError? = nil
.widgetAccentedRenderingMode(.accentedDesaturated) var deepLink: URL? = nil
} else {
self // Resizes the stored image to a maximum width of 450 pixels
mutating func resize() {
if (image == nil || image!.size.height < 450 || image!.size.width < 450 ) {
return
}
image = image?.resized(toWidth: 450)
if image == nil {
error = .unableToResize
} }
} }
} }
@@ -20,8 +29,7 @@ struct ImmichWidgetView: View {
if entry.image == nil { if entry.image == nil {
VStack { VStack {
Image("LaunchImage") Image("LaunchImage")
.tintedWidgetImageModifier() Text(entry.error?.errorDescription ?? "")
Text(entry.metadata.error?.errorDescription ?? "")
.minimumScaleFactor(0.25) .minimumScaleFactor(0.25)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -32,13 +40,11 @@ struct ImmichWidgetView: View {
Color.clear.overlay( Color.clear.overlay(
Image(uiImage: entry.image!) Image(uiImage: entry.image!)
.resizable() .resizable()
.tintedWidgetImageModifier()
.scaledToFill() .scaledToFill()
) )
VStack { VStack {
Spacer() Spacer()
if let subtitle = entry.metadata.subtitle { if let subtitle = entry.subtitle {
Text(subtitle) Text(subtitle)
.foregroundColor(.white) .foregroundColor(.white)
.padding(8) .padding(8)
@@ -49,7 +55,7 @@ struct ImmichWidgetView: View {
} }
.padding(16) .padding(16)
} }
.widgetURL(entry.metadata.deepLink) .widgetURL(entry.deepLink)
} }
} }
} }
@@ -64,9 +70,7 @@ struct ImmichWidgetView: View {
ImageEntry( ImageEntry(
date: date, date: date,
image: UIImage(named: "ImmichLogo"), image: UIImage(named: "ImmichLogo"),
metadata: EntryMetadata( subtitle: "1 year ago"
subtitle: "1 year ago"
)
) )
} }
) )

View File

@@ -2,20 +2,14 @@ import Foundation
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
let IMMICH_SHARE_GROUP = "group.app.immich.share" enum WidgetError: Error {
enum WidgetError: Error, Codable {
case noLogin case noLogin
case fetchFailed case fetchFailed
case unknown
case albumNotFound case albumNotFound
case noAssetsAvailable
}
enum FetchError: Error {
case unableToResize case unableToResize
case invalidImage case invalidImage
case invalidURL case invalidURL
case fetchFailed
} }
extension WidgetError: LocalizedError { extension WidgetError: LocalizedError {
@@ -30,8 +24,14 @@ extension WidgetError: LocalizedError {
case .albumNotFound: case .albumNotFound:
return "Album not found" return "Album not found"
case .noAssetsAvailable: case .invalidURL:
return "No assets available" return "An invalid URL was used"
case .invalidImage:
return "An invalid image was received"
default:
return "An unknown error occured"
} }
} }
} }
@@ -52,11 +52,10 @@ struct Asset: Codable {
} }
} }
struct SearchFilter: Codable { struct SearchFilters: Codable {
var type = AssetType.image var type: AssetType = .image
var size = 1 let size: Int
var albumIds: [String] = [] var albumIds: [String] = []
var isFavorite: Bool? = nil
} }
struct MemoryResult: Codable { struct MemoryResult: Codable {
@@ -71,34 +70,9 @@ struct MemoryResult: Codable {
let data: MemoryData let data: MemoryData
} }
struct Album: Codable, Equatable { struct Album: Codable {
let id: String let id: String
let albumName: String let albumName: String
static let NONE = Album(id: "NONE", albumName: "None")
static let FAVORITES = Album(id: "FAVORITES", albumName: "Favorites")
var filter: SearchFilter {
switch self {
case Album.NONE:
return SearchFilter()
case Album.FAVORITES:
return SearchFilter(isFavorite: true)
// regular album
default:
return SearchFilter(albumIds: [id])
}
}
var isVirtual: Bool {
switch self {
case Album.NONE, Album.FAVORITES:
return true
default:
return false
}
}
} }
// MARK: API // MARK: API
@@ -112,7 +86,7 @@ class ImmichAPI {
init() async throws { init() async throws {
// fetch the credentials from the UserDefaults store that dart placed here // fetch the credentials from the UserDefaults store that dart placed here
guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP), guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
let serverURL = defaults.string(forKey: "widget_server_url"), let serverURL = defaults.string(forKey: "widget_server_url"),
let sessionKey = defaults.string(forKey: "widget_auth_token") let sessionKey = defaults.string(forKey: "widget_auth_token")
else { else {
@@ -156,8 +130,7 @@ class ImmichAPI {
return components?.url return components?.url
} }
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter) func fetchSearchResults(with filters: SearchFilters) async throws
async throws
-> [Asset] -> [Asset]
{ {
// get URL // get URL
@@ -203,7 +176,7 @@ class ImmichAPI {
return try JSONDecoder().decode([MemoryResult].self, from: data) return try JSONDecoder().decode([MemoryResult].self, from: data)
} }
func fetchImage(asset: Asset) async throws(FetchError) -> UIImage { func fetchImage(asset: Asset) async throws(WidgetError) -> UIImage {
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")] let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
let assetEndpoint = "/assets/" + asset.id + "/thumbnail" let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
@@ -217,24 +190,17 @@ class ImmichAPI {
throw .invalidURL throw .invalidURL
} }
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
else {
throw .invalidURL throw .invalidURL
} }
let decodeOptions: [NSString: Any] = [ let decodeOptions: [NSString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: 512, kCGImageSourceThumbnailMaxPixelSize: 400,
kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailWithTransform: true
] ]
guard guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
let thumbnail = CGImageSourceCreateThumbnailAtIndex(
imageSource,
0,
decodeOptions as CFDictionary
)
else {
throw .fetchFailed throw .fetchFailed
} }

View File

@@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@@ -7,17 +7,14 @@
import UIKit import UIKit
extension UIImage { extension UIImage {
/// Crops the image to ensure width and height do not exceed maxSize. /// Crops the image to ensure width and height do not exceed maxSize.
/// Keeps original aspect ratio and crops excess equally from edges (center crop). /// Keeps original aspect ratio and crops excess equally from edges (center crop).
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? { func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
let canvas = CGSize( let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
width: width, let format = imageRendererFormat
height: CGFloat(ceil(width / size.width * size.height)) format.opaque = isOpaque
) return UIGraphicsImageRenderer(size: canvas, format: format).image {
let format = imageRendererFormat _ in draw(in: CGRect(origin: .zero, size: canvas))
format.opaque = isOpaque }
return UIGraphicsImageRenderer(size: canvas, format: format).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
} }
}
} }

View File

@@ -19,31 +19,28 @@ struct ImmichMemoryProvider: TimelineProvider {
in context: Context, in context: Context,
completion: @escaping @Sendable (ImageEntry) -> Void completion: @escaping @Sendable (ImageEntry) -> Void
) { ) {
let cacheKey = "memory_\(context.family.rawValue)"
Task { Task {
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
completion( completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
ImageEntry.handleError(for: cacheKey, error: .noLogin).entries.first!
)
return return
} }
guard let memories = try? await api.fetchMemory(for: Date.now) guard let memories = try? await api.fetchMemory(for: Date.now)
else { else {
completion(ImageEntry.handleError(for: cacheKey).entries.first!) completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return return
} }
for memory in memories { for memory in memories {
if let asset = memory.assets.first(where: { $0.type == .image }), if let asset = memory.assets.first(where: { $0.type == .image }),
let entry = try? await ImageEntry.build( var entry = try? await buildEntry(
api: api, api: api,
asset: asset, asset: asset,
dateOffset: 0, dateOffset: 0,
subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year) subtitle: getYearDifferenceSubtitle(assetYear: memory.data.year)
) )
{ {
entry.resize()
completion(entry) completion(entry)
return return
} }
@@ -51,17 +48,26 @@ struct ImmichMemoryProvider: TimelineProvider {
// fallback to random image // fallback to random image
guard guard
let randomImage = try? await api.fetchSearchResults().first, let randomImage = try? await api.fetchSearchResults(
let imageEntry = try? await ImageEntry.build( with: SearchFilters(size: 1)
).first
else {
completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return
}
guard
var imageEntry = try? await buildEntry(
api: api, api: api,
asset: randomImage, asset: randomImage,
dateOffset: 0 dateOffset: 0
) )
else { else {
completion(ImageEntry.handleError(for: cacheKey).entries.first!) completion(ImageEntry(date: Date(), image: nil, error: .fetchFailed))
return return
} }
imageEntry.resize()
completion(imageEntry) completion(imageEntry)
} }
} }
@@ -74,12 +80,9 @@ struct ImmichMemoryProvider: TimelineProvider {
var entries: [ImageEntry] = [] var entries: [ImageEntry] = []
let now = Date() let now = Date()
let cacheKey = "memory_\(context.family.rawValue)"
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
completion( entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
ImageEntry.handleError(for: cacheKey, error: .noLogin) completion(Timeline(entries: entries, policy: .atEnd))
)
return return
} }
@@ -92,7 +95,7 @@ struct ImmichMemoryProvider: TimelineProvider {
for asset in memory.assets { for asset in memory.assets {
if asset.type == .image && totalAssets < 12 { if asset.type == .image && totalAssets < 12 {
group.addTask { group.addTask {
try? await ImageEntry.build( try? await buildEntry(
api: api, api: api,
asset: asset, asset: asset,
dateOffset: totalAssets, dateOffset: totalAssets,
@@ -117,32 +120,25 @@ struct ImmichMemoryProvider: TimelineProvider {
// If we didnt add any memory images (some failure occured or no images in memory), // If we didnt add any memory images (some failure occured or no images in memory),
// default to 12 hours of random photos // default to 12 hours of random photos
if entries.count == 0 { if entries.count == 0 {
// this must be a do/catch since we need to entries.append(
// distinguish between a network fail and an empty search contentsOf: (try? await generateRandomEntries(
do {
let search = try await generateRandomEntries(
api: api, api: api,
now: now, now: now,
count: 12 count: 12
) )) ?? []
)
// Load or save a cached asset for when network conditions are bad
if search.count == 0 {
completion(
ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
)
return
}
entries.append(contentsOf: search)
} catch {
completion(ImageEntry.handleError(for: cacheKey))
return
}
} }
// cache the last image // If we fail to fetch images, we still want to add an entry
try? entries.last!.cache(for: cacheKey) // with a nil image and an error
if entries.count == 0 {
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
}
// Resize all images to something that can be stored by iOS
for i in entries.indices {
entries[i].resize()
}
completion(Timeline(entries: entries, policy: .atEnd)) completion(Timeline(entries: entries, policy: .atEnd))
} }

View File

@@ -8,21 +8,20 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
struct AlbumQuery: EntityQuery { struct AlbumQuery: EntityQuery {
func entities(for identifiers: [Album.ID]) async throws -> [Album] { func entities(for identifiers: [Album.ID]) async throws -> [Album] {
return await suggestedEntities().filter { // use cached albums to search
var albums = (try? await AlbumCache.shared.getAlbums()) ?? []
albums.insert(NO_ALBUM, at: 0)
return albums.filter {
identifiers.contains($0.id) identifiers.contains($0.id)
} }
} }
func suggestedEntities() async -> [Album] { func suggestedEntities() async throws -> [Album] {
let albums = (try? await AlbumCache.shared.getAlbums()) ?? [] var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
albums.insert(NO_ALBUM, at: 0)
let options = return albums
[
NONE,
FAVORITES,
] + albums
return options
} }
} }
@@ -36,6 +35,8 @@ extension Album: @unchecked Sendable, AppEntity, Identifiable {
} }
} }
let NO_ALBUM = Album(id: "NONE", albumName: "None")
struct RandomConfigurationAppIntent: WidgetConfigurationIntent { struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Select Album" } static var title: LocalizedStringResource { "Select Album" }
static var description: IntentDescription { static var description: IntentDescription {
@@ -53,7 +54,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
struct ImmichRandomProvider: AppIntentTimelineProvider { struct ImmichRandomProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> ImageEntry { func placeholder(in context: Context) -> ImageEntry {
ImageEntry(date: Date()) ImageEntry(date: Date(), image: nil)
} }
func snapshot( func snapshot(
@@ -62,26 +63,30 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
) async ) async
-> ImageEntry -> ImageEntry
{ {
let cacheKey = "random_none_\(context.family.rawValue)"
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries return ImageEntry(date: Date(), image: nil, error: .noLogin)
.first!
} }
guard guard
let randomImage = try? await api.fetchSearchResults( let randomImage = try? await api.fetchSearchResults(
with: Album.NONE.filter with: SearchFilters(size: 1)
).first, ).first
let entry = try? await ImageEntry.build( else {
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
}
guard
var entry = try? await buildEntry(
api: api, api: api,
asset: randomImage, asset: randomImage,
dateOffset: 0 dateOffset: 0
) )
else { else {
return ImageEntry.handleError(for: cacheKey).entries.first! return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
} }
entry.resize()
return entry return entry
} }
@@ -94,41 +99,50 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
var entries: [ImageEntry] = [] var entries: [ImageEntry] = []
let now = Date() let now = Date()
// nil if album is NONE or nil
let album = configuration.album ?? Album.NONE
let albumName = album.isVirtual ? nil : album.albumName
let cacheKey = "random_\(album.id)_\(context.family.rawValue)"
// If we don't have a server config, return an entry with an error // If we don't have a server config, return an entry with an error
guard let api = try? await ImmichAPI() else { guard let api = try? await ImmichAPI() else {
return ImageEntry.handleError(for: cacheKey, error: .noLogin) entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
return Timeline(entries: entries, policy: .atEnd)
} }
// build entries // nil if album is NONE or nil
// this must be a do/catch since we need to let albumId =
// distinguish between a network fail and an empty search configuration.album?.id != "NONE" ? configuration.album?.id : nil
do { var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
let search = try await generateRandomEntries(
if albumId != nil {
// make sure the album exists on server, otherwise show error
guard let albums = try? await api.fetchAlbums() else {
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
return Timeline(entries: entries, policy: .atEnd)
}
if !albums.contains(where: { $0.id == albumId }) {
entries.append(ImageEntry(date: now, image: nil, error: .albumNotFound))
return Timeline(entries: entries, policy: .atEnd)
}
}
entries.append(
contentsOf: (try? await generateRandomEntries(
api: api, api: api,
now: now, now: now,
count: 12, count: 12,
filter: album.filter, albumId: albumId,
subtitle: configuration.showAlbumName ? albumName : nil subtitle: configuration.showAlbumName ? albumName : nil
) ))
?? []
)
// Load or save a cached asset for when network conditions are bad // If we fail to fetch images, we still want to add an entry with a nil image and an error
if search.count == 0 { if entries.count == 0 {
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable) entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
}
entries.append(contentsOf: search)
} catch {
return ImageEntry.handleError(for: cacheKey)
} }
// cache the last image // Resize all images to something that can be stored by iOS
try? entries.last!.cache(for: cacheKey) for i in entries.indices {
entries[i].resize()
}
return Timeline(entries: entries, policy: .atEnd) return Timeline(entries: entries, policy: .atEnd)
} }

View File

@@ -18,7 +18,7 @@ const String kSecuredPinCode = "secured_pin_code";
// Timeline constants // Timeline constants
const int kTimelineNoneSegmentSize = 120; const int kTimelineNoneSegmentSize = 120;
const int kTimelineAssetLoadBatchSize = 256; const int kTimelineAssetLoadBatchSize = 1024;
const int kTimelineAssetLoadOppositeSize = 64; const int kTimelineAssetLoadOppositeSize = 64;
// Widget keys // Widget keys

View File

@@ -11,7 +11,7 @@ enum AlbumUserRole {
} }
// Model for an album stored in the server // Model for an album stored in the server
class RemoteAlbum { class Album {
final String id; final String id;
final String name; final String name;
final String ownerId; final String ownerId;
@@ -24,7 +24,7 @@ class RemoteAlbum {
final int assetCount; final int assetCount;
final String ownerName; final String ownerName;
const RemoteAlbum({ const Album({
required this.id, required this.id,
required this.name, required this.name,
required this.ownerId, required this.ownerId,
@@ -57,7 +57,7 @@ class RemoteAlbum {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! RemoteAlbum) return false; if (other is! Album) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return id == other.id && return id == other.id &&
name == other.name && name == other.name &&
@@ -86,32 +86,4 @@ class RemoteAlbum {
assetCount.hashCode ^ assetCount.hashCode ^
ownerName.hashCode; ownerName.hashCode;
} }
RemoteAlbum copyWith({
String? id,
String? name,
String? ownerId,
String? description,
DateTime? createdAt,
DateTime? updatedAt,
String? thumbnailAssetId,
bool? isActivityEnabled,
AlbumAssetOrder? order,
int? assetCount,
String? ownerName,
}) {
return RemoteAlbum(
id: id ?? this.id,
name: name ?? this.name,
ownerId: ownerId ?? this.ownerId,
description: description ?? this.description,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled,
order: order ?? this.order,
assetCount: assetCount ?? this.assetCount,
ownerName: ownerName ?? this.ownerName,
);
}
} }

View File

@@ -25,7 +25,6 @@ sealed class BaseAsset {
final int? height; final int? height;
final int? durationInSeconds; final int? durationInSeconds;
final bool isFavorite; final bool isFavorite;
final String? livePhotoVideoId;
const BaseAsset({ const BaseAsset({
required this.name, required this.name,
@@ -37,20 +36,16 @@ sealed class BaseAsset {
this.height, this.height,
this.durationInSeconds, this.durationInSeconds,
this.isFavorite = false, this.isFavorite = false,
this.livePhotoVideoId,
}); });
bool get isImage => type == AssetType.image; bool get isImage => type == AssetType.image;
bool get isVideo => type == AssetType.video; bool get isVideo => type == AssetType.video;
bool get isMotionPhoto => livePhotoVideoId != null; double? get aspectRatio {
if (width != null && height != null && height! > 0) {
Duration get duration { return width! / height!;
final durationInSeconds = this.durationInSeconds;
if (durationInSeconds != null) {
return Duration(seconds: durationInSeconds);
} }
return const Duration(); return null;
} }
bool get hasRemote => bool get hasRemote =>

View File

@@ -3,7 +3,6 @@ part of 'base_asset.model.dart';
class LocalAsset extends BaseAsset { class LocalAsset extends BaseAsset {
final String id; final String id;
final String? remoteId; final String? remoteId;
final int orientation;
const LocalAsset({ const LocalAsset({
required this.id, required this.id,
@@ -17,8 +16,6 @@ class LocalAsset extends BaseAsset {
super.height, super.height,
super.durationInSeconds, super.durationInSeconds,
super.isFavorite = false, super.isFavorite = false,
super.livePhotoVideoId,
this.orientation = 0,
}); });
@override @override
@@ -41,7 +38,6 @@ class LocalAsset extends BaseAsset {
durationInSeconds: ${durationInSeconds ?? "<NA>"}, durationInSeconds: ${durationInSeconds ?? "<NA>"},
remoteId: ${remoteId ?? "<NA>"} remoteId: ${remoteId ?? "<NA>"}
isFavorite: $isFavorite, isFavorite: $isFavorite,
orientation: $orientation,
}'''; }''';
} }
@@ -49,12 +45,11 @@ class LocalAsset extends BaseAsset {
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAsset) return false; if (other is! LocalAsset) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return super == other && id == other.id && orientation == other.orientation; return super == other && id == other.id && remoteId == other.remoteId;
} }
@override @override
int get hashCode => int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
LocalAsset copyWith({ LocalAsset copyWith({
String? id, String? id,
@@ -68,7 +63,6 @@ class LocalAsset extends BaseAsset {
int? height, int? height,
int? durationInSeconds, int? durationInSeconds,
bool? isFavorite, bool? isFavorite,
int? orientation,
}) { }) {
return LocalAsset( return LocalAsset(
id: id ?? this.id, id: id ?? this.id,
@@ -82,7 +76,6 @@ class LocalAsset extends BaseAsset {
height: height ?? this.height, height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds, durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
); );
} }
} }

View File

@@ -30,7 +30,6 @@ class RemoteAsset extends BaseAsset {
super.isFavorite = false, super.isFavorite = false,
this.thumbHash, this.thumbHash,
this.visibility = AssetVisibility.timeline, this.visibility = AssetVisibility.timeline,
super.livePhotoVideoId,
}); });
@override @override
@@ -66,6 +65,7 @@ class RemoteAsset extends BaseAsset {
return super == other && return super == other &&
id == other.id && id == other.id &&
ownerId == other.ownerId && ownerId == other.ownerId &&
localId == other.localId &&
thumbHash == other.thumbHash && thumbHash == other.thumbHash &&
visibility == other.visibility; visibility == other.visibility;
} }
@@ -94,7 +94,6 @@ class RemoteAsset extends BaseAsset {
bool? isFavorite, bool? isFavorite,
String? thumbHash, String? thumbHash,
AssetVisibility? visibility, AssetVisibility? visibility,
String? livePhotoVideoId,
}) { }) {
return RemoteAsset( return RemoteAsset(
id: id ?? this.id, id: id ?? this.id,
@@ -111,7 +110,6 @@ class RemoteAsset extends BaseAsset {
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash, thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
); );
} }
} }

View File

@@ -5,7 +5,6 @@ enum Setting<T> {
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0), groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
showStorageIndicator<bool>(StoreKey.storageIndicator, true), showStorageIndicator<bool>(StoreKey.storageIndicator, true),
loadOriginal<bool>(StoreKey.loadOriginal, false), loadOriginal<bool>(StoreKey.loadOriginal, false),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false), preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
; ;

View File

@@ -1,12 +1,5 @@
import 'dart:ui'; import 'dart:ui';
enum UserMetadataKey {
// do not change this order!
onboarding,
preferences,
license,
}
enum AvatarColor { enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK // do not change this order or reuse indices for other purposes, adding is OK
primary("primary"), primary("primary"),
@@ -38,45 +31,7 @@ enum AvatarColor {
}; };
} }
class Onboarding { class UserPreferences {
final bool isOnboarded;
const Onboarding({required this.isOnboarded});
Onboarding copyWith({bool? isOnboarded}) {
return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded);
}
Map<String, Object?> toMap() {
final onboarding = <String, Object?>{};
onboarding["isOnboarded"] = isOnboarded;
return onboarding;
}
factory Onboarding.fromMap(Map<String, Object?> map) {
return Onboarding(isOnboarded: map["isOnboarded"] as bool);
}
@override
String toString() {
return '''Onboarding {
isOnboarded: $isOnboarded,
}''';
}
@override
bool operator ==(covariant Onboarding other) {
if (identical(this, other)) return true;
return isOnboarded == other.isOnboarded;
}
@override
int get hashCode => isOnboarded.hashCode;
}
// TODO: wait to be overwritten
class Preferences {
final bool foldersEnabled; final bool foldersEnabled;
final bool memoriesEnabled; final bool memoriesEnabled;
final bool peopleEnabled; final bool peopleEnabled;
@@ -86,7 +41,7 @@ class Preferences {
final AvatarColor userAvatarColor; final AvatarColor userAvatarColor;
final bool showSupportBadge; final bool showSupportBadge;
const Preferences({ const UserPreferences({
this.foldersEnabled = false, this.foldersEnabled = false,
this.memoriesEnabled = true, this.memoriesEnabled = true,
this.peopleEnabled = true, this.peopleEnabled = true,
@@ -97,7 +52,7 @@ class Preferences {
this.showSupportBadge = true, this.showSupportBadge = true,
}); });
Preferences copyWith({ UserPreferences copyWith({
bool? foldersEnabled, bool? foldersEnabled,
bool? memoriesEnabled, bool? memoriesEnabled,
bool? peopleEnabled, bool? peopleEnabled,
@@ -107,7 +62,7 @@ class Preferences {
AvatarColor? userAvatarColor, AvatarColor? userAvatarColor,
bool? showSupportBadge, bool? showSupportBadge,
}) { }) {
return Preferences( return UserPreferences(
foldersEnabled: foldersEnabled ?? this.foldersEnabled, foldersEnabled: foldersEnabled ?? this.foldersEnabled,
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled, memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
peopleEnabled: peopleEnabled ?? this.peopleEnabled, peopleEnabled: peopleEnabled ?? this.peopleEnabled,
@@ -132,8 +87,8 @@ class Preferences {
return preferences; return preferences;
} }
factory Preferences.fromMap(Map<String, Object?> map) { factory UserPreferences.fromMap(Map<String, Object?> map) {
return Preferences( return UserPreferences(
foldersEnabled: map["folders-Enabled"] as bool? ?? false, foldersEnabled: map["folders-Enabled"] as bool? ?? false,
memoriesEnabled: map["memories-Enabled"] as bool? ?? true, memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
peopleEnabled: map["people-Enabled"] as bool? ?? true, peopleEnabled: map["people-Enabled"] as bool? ?? true,
@@ -147,173 +102,4 @@ class Preferences {
showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true, showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
); );
} }
@override
String toString() {
return '''Preferences: {
foldersEnabled: $foldersEnabled,
memoriesEnabled: $memoriesEnabled,
peopleEnabled: $peopleEnabled,
ratingsEnabled: $ratingsEnabled,
sharedLinksEnabled: $sharedLinksEnabled,
tagsEnabled: $tagsEnabled,
userAvatarColor: $userAvatarColor,
showSupportBadge: $showSupportBadge,
}''';
}
@override
bool operator ==(covariant Preferences other) {
if (identical(this, other)) return true;
return other.foldersEnabled == foldersEnabled &&
other.memoriesEnabled == memoriesEnabled &&
other.peopleEnabled == peopleEnabled &&
other.ratingsEnabled == ratingsEnabled &&
other.sharedLinksEnabled == sharedLinksEnabled &&
other.tagsEnabled == tagsEnabled &&
other.userAvatarColor == userAvatarColor &&
other.showSupportBadge == showSupportBadge;
}
@override
int get hashCode {
return foldersEnabled.hashCode ^
memoriesEnabled.hashCode ^
peopleEnabled.hashCode ^
ratingsEnabled.hashCode ^
sharedLinksEnabled.hashCode ^
tagsEnabled.hashCode ^
userAvatarColor.hashCode ^
showSupportBadge.hashCode;
}
}
class License {
final DateTime activatedAt;
final String activationKey;
final String licenseKey;
const License({
required this.activatedAt,
required this.activationKey,
required this.licenseKey,
});
License copyWith({
DateTime? activatedAt,
String? activationKey,
String? licenseKey,
}) {
return License(
activatedAt: activatedAt ?? this.activatedAt,
activationKey: activationKey ?? this.activationKey,
licenseKey: licenseKey ?? this.licenseKey,
);
}
Map<String, Object?> toMap() {
final license = <String, Object?>{};
license["activatedAt"] = activatedAt;
license["activationKey"] = activationKey;
license["licenseKey"] = licenseKey;
return license;
}
factory License.fromMap(Map<String, Object?> map) {
return License(
activatedAt: map["activatedAt"] as DateTime,
activationKey: map["activationKey"] as String,
licenseKey: map["licenseKey"] as String,
);
}
@override
String toString() {
return '''License {
activatedAt: $activatedAt,
activationKey: $activationKey,
licenseKey: $licenseKey,
}''';
}
@override
bool operator ==(covariant License other) {
if (identical(this, other)) return true;
return activatedAt == other.activatedAt &&
activationKey == other.activationKey &&
licenseKey == other.licenseKey;
}
@override
int get hashCode =>
activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode;
}
// Model for a user metadata stored in the server
class UserMetadata {
final String userId;
final UserMetadataKey key;
final Onboarding? onboarding;
final Preferences? preferences;
final License? license;
const UserMetadata({
required this.userId,
required this.key,
this.onboarding,
this.preferences,
this.license,
}) : assert(
onboarding != null || preferences != null || license != null,
'One of onboarding, preferences and license must be provided',
);
UserMetadata copyWith({
String? userId,
UserMetadataKey? key,
Onboarding? onboarding,
Preferences? preferences,
License? license,
}) {
return UserMetadata(
userId: userId ?? this.userId,
key: key ?? this.key,
onboarding: onboarding ?? this.onboarding,
preferences: preferences ?? this.preferences,
license: license ?? this.license,
);
}
@override
String toString() {
return '''UserMetadata: {
userId: $userId,
key: $key,
onboarding: ${onboarding ?? "<NA>"},
preferences: ${preferences ?? "<NA>"},
license: ${license ?? "<NA>"},
}''';
}
@override
bool operator ==(covariant UserMetadata other) {
if (identical(this, other)) return true;
return other.userId == userId &&
other.key == key &&
other.onboarding == onboarding &&
other.preferences == preferences &&
other.license == license;
}
@override
int get hashCode {
return userId.hashCode ^
key.hashCode ^
onboarding.hashCode ^
preferences.hashCode ^
license.hashCode;
}
} }

View File

@@ -2,20 +2,16 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:platform/platform.dart';
class AssetService { class AssetService {
final RemoteAssetRepository _remoteAssetRepository; final RemoteAssetRepository _remoteAssetRepository;
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final Platform _platform;
const AssetService({ const AssetService({
required RemoteAssetRepository remoteAssetRepository, required RemoteAssetRepository remoteAssetRepository,
required DriftLocalAssetRepository localAssetRepository, required DriftLocalAssetRepository localAssetRepository,
}) : _remoteAssetRepository = remoteAssetRepository, }) : _remoteAssetRepository = remoteAssetRepository,
_localAssetRepository = localAssetRepository, _localAssetRepository = localAssetRepository;
_platform = const LocalPlatform();
Stream<BaseAsset?> watchAsset(BaseAsset asset) { Stream<BaseAsset?> watchAsset(BaseAsset asset) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id; final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
@@ -25,44 +21,10 @@ class AssetService {
} }
Future<ExifInfo?> getExif(BaseAsset asset) async { Future<ExifInfo?> getExif(BaseAsset asset) async {
if (!asset.hasRemote) { if (asset is LocalAsset || asset is! RemoteAsset) {
return null; return null;
} }
final id = return _remoteAssetRepository.getExif(asset.id);
asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
return _remoteAssetRepository.getExif(id);
}
Future<double> getAspectRatio(BaseAsset asset) async {
bool isFlipped;
double? width;
double? height;
if (asset.hasRemote) {
final exif = await getExif(asset);
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
width = exif?.width ?? asset.width?.toDouble();
height = exif?.height ?? asset.height?.toDouble();
} else if (asset is LocalAsset) {
isFlipped = _platform.isAndroid &&
(asset.orientation == 90 || asset.orientation == 270);
width = asset.width?.toDouble();
height = asset.height?.toDouble();
} else {
isFlipped = false;
}
final orientedWidth = isFlipped ? height : width;
final orientedHeight = isFlipped ? width : height;
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
return orientedWidth / orientedHeight;
}
return 1.0;
}
Future<List<(String, String)>> getPlaces() {
return _remoteAssetRepository.getPlaces();
} }
} }

View File

@@ -61,7 +61,7 @@ class HashService {
final toHash = <_AssetToPath>[]; final toHash = <_AssetToPath>[];
for (final asset in assetsToHash) { for (final asset in assetsToHash) {
final file = await _storageRepository.getFileForAsset(asset.id); final file = await _storageRepository.getFileForAsset(asset);
if (file == null) { if (file == null) {
continue; continue;
} }

View File

@@ -1,17 +0,0 @@
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/repositories/local_album.repository.dart';
class LocalAlbumService {
final DriftLocalAlbumRepository _repository;
const LocalAlbumService(this._repository);
Future<List<LocalAlbum>> getAll() {
return _repository.getAll();
}
Future<LocalAsset?> getThumbnail(String albumId) {
return _repository.getThumbnail(albumId);
}
}

View File

@@ -359,7 +359,6 @@ extension on Iterable<PlatformAsset> {
width: e.width, width: e.width,
height: e.height, height: e.height,
durationInSeconds: e.durationInSeconds, durationInSeconds: e.durationInSeconds,
orientation: e.orientation,
), ),
).toList(); ).toList();
} }

View File

@@ -1,43 +1,33 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/utils/remote_album.utils.dart'; import 'package:immich_mobile/utils/remote_album.utils.dart';
class RemoteAlbumService { class RemoteAlbumService {
final DriftRemoteAlbumRepository _repository; final DriftRemoteAlbumRepository _repository;
final DriftAlbumApiRepository _albumApiRepository;
const RemoteAlbumService(this._repository, this._albumApiRepository); const RemoteAlbumService(this._repository);
Stream<RemoteAlbum?> watchAlbum(String albumId) { Future<List<Album>> getAll() {
return _repository.watchAlbum(albumId);
}
Future<List<RemoteAlbum>> getAll() {
return _repository.getAll(); return _repository.getAll();
} }
List<RemoteAlbum> sortAlbums( List<Album> sortAlbums(
List<RemoteAlbum> albums, List<Album> albums,
RemoteAlbumSortMode sortMode, { RemoteAlbumSortMode sortMode, {
bool isReverse = false, bool isReverse = false,
}) { }) {
return sortMode.sortFn(albums, isReverse); return sortMode.sortFn(albums, isReverse);
} }
List<RemoteAlbum> searchAlbums( List<Album> searchAlbums(
List<RemoteAlbum> albums, List<Album> albums,
String query, String query,
String? userId, [ String? userId, [
QuickFilterMode filterMode = QuickFilterMode.all, QuickFilterMode filterMode = QuickFilterMode.all,
]) { ]) {
final lowerQuery = query.toLowerCase(); final lowerQuery = query.toLowerCase();
List<RemoteAlbum> filtered = albums; List<Album> filtered = albums;
// Apply text search filter // Apply text search filter
if (query.isNotEmpty) { if (query.isNotEmpty) {
@@ -67,84 +57,4 @@ class RemoteAlbumService {
return filtered; return filtered;
} }
Future<RemoteAlbum> createAlbum({
required String title,
required List<String> assetIds,
String? description,
}) async {
final album = await _albumApiRepository.createDriftAlbum(
title,
description: description,
assetIds: assetIds,
);
await _repository.create(album, assetIds);
return album;
}
Future<RemoteAlbum> updateAlbum(
String albumId, {
String? name,
String? description,
String? thumbnailAssetId,
bool? isActivityEnabled,
AlbumAssetOrder? order,
}) async {
final updatedAlbum = await _albumApiRepository.updateAlbum(
albumId,
name: name,
description: description,
thumbnailAssetId: thumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order,
);
// Update the local database
await _repository.update(updatedAlbum);
return updatedAlbum;
}
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
return _repository.getDateRange(albumId);
}
Future<List<UserDto>> getSharedUsers(String albumId) {
return _repository.getSharedUsers(albumId);
}
Future<List<RemoteAsset>> getAssets(String albumId) {
return _repository.getAssets(albumId);
}
Future<int> addAssets({
required String albumId,
required List<String> assetIds,
}) async {
final album = await _albumApiRepository.addAssets(
albumId,
assetIds,
);
await _repository.addAssets(albumId, album.added);
return album.added.length;
}
Future<void> deleteAlbum(String albumId) async {
await _albumApiRepository.deleteAlbum(albumId);
await _repository.deleteAlbum(albumId);
}
Future<void> addUsers({
required String albumId,
required List<String> userIds,
}) async {
await _albumApiRepository.addUsers(albumId, userIds);
return _repository.addUsers(albumId, userIds);
}
} }

View File

@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -24,65 +23,7 @@ class SyncStreamService {
bool get isCancelled => _cancelChecker?.call() ?? false; bool get isCancelled => _cancelChecker?.call() ?? false;
Future<void> sync() { Future<void> sync() => _syncApiRepository.streamChanges(_handleEvents);
_logger.info("Remote sync request for userr");
DLog.log("Remote sync request for user");
// Start the sync stream and handle events
return _syncApiRepository.streamChanges(_handleEvents);
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info(
'Processing batch of ${batchData.length} AssetUploadReadyV1 events',
);
final List<SyncAssetV1> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV1.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(
assets,
debugLabel: 'websocket-batch',
);
await _syncStreamRepository.updateAssetsExifV1(
exifs,
debugLabel: 'websocket-batch',
);
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe(
"Error processing AssetUploadReadyV1 websocket batch events",
error,
stackTrace,
);
}
}
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async { Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
List<SyncEvent> items = []; List<SyncEvent> items = [];
@@ -232,14 +173,6 @@ class SyncStreamService {
data.cast(), data.cast(),
debugLabel: 'partner', debugLabel: 'partner',
); );
case SyncEntityType.userMetadataV1:
return _syncStreamRepository.updateUserMetadatasV1(
data.cast(),
);
case SyncEntityType.userMetadataDeleteV1:
return _syncStreamRepository.deleteUserMetadatasV1(
data.cast(),
);
default: default:
_logger.warning("Unknown sync data type: $type"); _logger.warning("Unknown sync data type: $type");
} }

View File

@@ -18,11 +18,6 @@ typedef TimelineAssetSource = Future<List<BaseAsset>> Function(
typedef TimelineBucketSource = Stream<List<Bucket>> Function(); typedef TimelineBucketSource = Stream<List<Bucket>> Function();
typedef TimelineQuery = ({
TimelineAssetSource assetSource,
TimelineBucketSource bucketSource,
});
class TimelineFactory { class TimelineFactory {
final DriftTimelineRepository _timelineRepository; final DriftTimelineRepository _timelineRepository;
final SettingsService _settingsService; final SettingsService _settingsService;
@@ -36,95 +31,110 @@ class TimelineFactory {
GroupAssetsBy get groupBy => GroupAssetsBy get groupBy =>
GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)]; GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
TimelineService main(List<String> timelineUsers) => TimelineService main(List<String> timelineUsers) => TimelineService(
TimelineService(_timelineRepository.main(timelineUsers, groupBy)); assetSource: (offset, count) => _timelineRepository
.getMainBucketAssets(timelineUsers, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchMainBucket(
timelineUsers,
groupBy: groupBy,
),
);
TimelineService localAlbum({required String albumId}) => TimelineService localAlbum({required String albumId}) => TimelineService(
TimelineService(_timelineRepository.localAlbum(albumId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getLocalAlbumBucketAssets(albumId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchLocalAlbumBucket(
albumId,
groupBy: groupBy,
),
);
TimelineService remoteAlbum({required String albumId}) => TimelineService remoteAlbum({required String albumId}) => TimelineService(
TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getRemoteAlbumBucketAssets(albumId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteAlbumBucket(
albumId,
groupBy: groupBy,
),
);
TimelineService remoteAssets(String userId) => TimelineService remoteAssets(List<String> timelineUsers) => TimelineService(
TimelineService(_timelineRepository.remote(userId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getRemoteBucketAssets(timelineUsers, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteBucket(
timelineUsers,
groupBy: GroupAssetsBy.month,
),
);
TimelineService favorite(String userId) => TimelineService favorite(String userId) => TimelineService(
TimelineService(_timelineRepository.favorite(userId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getFavoriteBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy),
);
TimelineService trash(String userId) => TimelineService trash(String userId) => TimelineService(
TimelineService(_timelineRepository.trash(userId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getTrashBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
);
TimelineService archive(String userId) => TimelineService archive(String userId) => TimelineService(
TimelineService(_timelineRepository.archived(userId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getArchiveBucketAssets(userId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
);
TimelineService lockedFolder(String userId) => TimelineService lockedFolder(String userId) => TimelineService(
TimelineService(_timelineRepository.locked(userId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
userId,
groupBy: groupBy,
),
);
TimelineService video(String userId) => TimelineService video(String userId) => TimelineService(
TimelineService(_timelineRepository.video(userId, groupBy)); assetSource: (offset, count) => _timelineRepository
.getVideoBucketAssets(userId, offset: offset, count: count),
TimelineService place(String place) => bucketSource: () =>
TimelineService(_timelineRepository.place(place, groupBy)); _timelineRepository.watchVideoBucket(userId, groupBy: groupBy),
);
} }
class TimelineService { class TimelineService {
final TimelineAssetSource _assetSource; final TimelineAssetSource _assetSource;
final TimelineBucketSource _bucketSource; final TimelineBucketSource _bucketSource;
final AsyncMutex _mutex = AsyncMutex();
int _bufferOffset = 0;
List<BaseAsset> _buffer = [];
StreamSubscription? _bucketSubscription;
int _totalAssets = 0; int _totalAssets = 0;
int get totalAssets => _totalAssets; int get totalAssets => _totalAssets;
TimelineService(TimelineQuery query) TimelineService({
: this._(
assetSource: query.assetSource,
bucketSource: query.bucketSource,
);
TimelineService._({
required TimelineAssetSource assetSource, required TimelineAssetSource assetSource,
required TimelineBucketSource bucketSource, required TimelineBucketSource bucketSource,
}) : _assetSource = assetSource, }) : _assetSource = assetSource,
_bucketSource = bucketSource { _bucketSource = bucketSource {
_bucketSubscription = _bucketSource().listen((buckets) { _bucketSubscription = _bucketSource().listen((buckets) {
_mutex.run(() async { _totalAssets =
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount); unawaited(_reloadBucket());
if (totalAssets == 0) {
_bufferOffset = 0;
_buffer.clear();
} else {
final int offset;
final int count;
// When the buffer is empty or the old bufferOffset is greater than the new total assets,
// we need to reset the buffer and load the first batch of assets.
if (_bufferOffset >= totalAssets || _buffer.isEmpty) {
offset = 0;
count = kTimelineAssetLoadBatchSize;
} else {
offset = _bufferOffset;
count = math.min(
_buffer.length,
totalAssets - _bufferOffset,
);
}
_buffer = await _assetSource(offset, count);
_bufferOffset = offset;
}
// change the state's total assets count only after the buffer is reloaded
_totalAssets = totalAssets;
EventStream.shared.emit(const TimelineReloadEvent());
});
}); });
} }
final AsyncMutex _mutex = AsyncMutex();
int _bufferOffset = 0;
List<BaseAsset> _buffer = [];
StreamSubscription? _bucketSubscription;
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource; Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
Future<void> _reloadBucket() => _mutex.run(() async {
_buffer = await _assetSource(_bufferOffset, _buffer.length);
EventStream.shared.emit(const TimelineReloadEvent());
});
Future<List<BaseAsset>> loadAssets(int index, int count) => Future<List<BaseAsset>> loadAssets(int index, int count) =>
_mutex.run(() => _loadAssets(index, count)); _mutex.run(() => _loadAssets(index, count));
@@ -153,20 +163,18 @@ class TimelineService {
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len), : (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
); );
_buffer = await _assetSource(start, len); final assets = await _assetSource(start, len);
_buffer = assets;
_bufferOffset = start; _bufferOffset = start;
return getAssets(index, count); return getAssets(index, count);
} }
bool hasRange(int index, int count) => bool hasRange(int index, int count) =>
index >= 0 && index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
index < _totalAssets &&
index >= _bufferOffset &&
index + count <= _bufferOffset + _buffer.length &&
index + count <= _totalAssets;
List<BaseAsset> getAssets(int index, int count) { List<BaseAsset> getAssets(int index, int count) {
assert(index + count <= totalAssets);
if (!hasRange(index, count)) { if (!hasRange(index, count)) {
throw RangeError('TimelineService::getAssets Index out of range'); throw RangeError('TimelineService::getAssets Index out of range');
} }
@@ -176,16 +184,11 @@ class TimelineService {
// Pre-cache assets around the given index for asset viewer // Pre-cache assets around the given index for asset viewer
Future<void> preCacheAssets(int index) => Future<void> preCacheAssets(int index) =>
_mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index))); _mutex.run(() => _loadAssets(index, 5));
BaseAsset getRandomAsset() =>
_buffer.elementAt(math.Random().nextInt(_buffer.length));
BaseAsset getAsset(int index) { BaseAsset getAsset(int index) {
if (!hasRange(index, 1)) { if (!hasRange(index, 1)) {
throw RangeError( throw RangeError('TimelineService::getAsset Index out of range');
'TimelineService::getAsset Index $index not in buffer range [$_bufferOffset, ${_bufferOffset + _buffer.length})',
);
} }
return _buffer.elementAt(index - _bufferOffset); return _buffer.elementAt(index - _bufferOffset);
} }

View File

@@ -6,7 +6,6 @@ import 'package:worker_manager/worker_manager.dart';
class BackgroundSyncManager { class BackgroundSyncManager {
Cancelable<void>? _syncTask; Cancelable<void>? _syncTask;
Cancelable<void>? _syncWebsocketTask;
Cancelable<void>? _deviceAlbumSyncTask; Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _hashTask; Cancelable<void>? _hashTask;
@@ -21,12 +20,6 @@ class BackgroundSyncManager {
_syncTask?.cancel(); _syncTask?.cancel();
_syncTask = null; _syncTask = null;
if (_syncWebsocketTask != null) {
futures.add(_syncWebsocketTask!.future);
}
_syncWebsocketTask?.cancel();
_syncWebsocketTask = null;
return Future.wait(futures); return Future.wait(futures);
} }
@@ -79,19 +72,4 @@ class BackgroundSyncManager {
_syncTask = null; _syncTask = null;
}); });
} }
Future<void> syncWebsocketBatch(List<dynamic> batchData) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
_syncWebsocketTask = runInIsolateGentle(
computation: (ref) => ref
.read(syncStreamServiceProvider)
.handleWsAssetUploadReadyV1Batch(batchData),
);
return _syncWebsocketTask!.whenComplete(() {
_syncWebsocketTask = null;
});
}
} }

View File

@@ -1,6 +1,3 @@
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
extension TimeAgoExtension on DateTime { extension TimeAgoExtension on DateTime {
/// Displays the time difference of this [DateTime] object to the current time as a [String] /// Displays the time difference of this [DateTime] object to the current time as a [String]
String timeAgo({bool numericDates = true}) { String timeAgo({bool numericDates = true}) {
@@ -38,56 +35,3 @@ extension TimeAgoExtension on DateTime {
return '${(difference.inDays / 365).floor()} years ago'; return '${(difference.inDays / 365).floor()} years ago';
} }
} }
/// Extension to format date ranges according to UI requirements
extension DateRangeFormatting on DateTime {
/// Formats a date range according to specific rules:
/// - Single date of this year: "Aug 28"
/// - Single date of other year: "Aug 28, 2023"
/// - Date range of this year: "Mar 23-May 31"
/// - Date range of other year: "Aug 28 - Sep 30, 2023"
/// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
static String formatDateRange(
DateTime startDate,
DateTime endDate,
Locale? locale,
) {
final now = DateTime.now();
final currentYear = now.year;
final localeString = locale?.toString() ?? 'en_US';
// Check if it's a single date (same day)
if (startDate.year == endDate.year &&
startDate.month == endDate.month &&
startDate.day == endDate.day) {
if (startDate.year == currentYear) {
// Single date of this year: "Aug 28"
return DateFormat.MMMd(localeString).format(startDate);
} else {
// Single date of other year: "Aug 28, 2023"
return DateFormat.yMMMd(localeString).format(startDate);
}
}
// It's a date range
if (startDate.year == endDate.year) {
// Same year
if (startDate.year == currentYear) {
// Date range of this year: "Mar 23-May 31"
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
return '$startFormatted - $endFormatted';
} else {
// Date range of other year: "Aug 28 - Sep 30, 2023"
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
return '$startFormatted - $endFormatted, ${startDate.year}';
}
} else {
// Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
final startFormatted = DateFormat.yMMMd(localeString).format(startDate);
final endFormatted = DateFormat.yMMMd(localeString).format(endDate);
return '$startFormatted - $endFormatted';
}
}
}

View File

@@ -3,15 +3,3 @@ extension TZOffsetExtension on Duration {
String formatAsOffset() => String formatAsOffset() =>
"${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}"; "${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
} }
extension DurationFormatExtension on Duration {
String format() {
final seconds = inSeconds.remainder(60).toString().padLeft(2, '0');
final minutes = inMinutes.remainder(60).toString().padLeft(2, '0');
if (inHours == 0) {
return "$minutes:$seconds";
}
final hours = inHours.toString().padLeft(2, '0');
return "$hours:$minutes:$seconds";
}
}

View File

@@ -11,9 +11,9 @@ class FastScrollPhysics extends ScrollPhysics {
@override @override
SpringDescription get spring => const SpringDescription( SpringDescription get spring => const SpringDescription(
mass: 1, mass: 40,
stiffness: 402.49984375, stiffness: 100,
damping: 40, damping: 1,
); );
} }
@@ -31,8 +31,8 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics {
// can briefly be seen and cause a flicker effect if the video begins to initialize // can briefly be seen and cause a flicker effect if the video begins to initialize
// before the animation finishes - probably a bug in PhotoViewGallery's animation handling // before the animation finishes - probably a bug in PhotoViewGallery's animation handling
// Making the animation faster is not just stylistic, but also helps to avoid this flicker // Making the animation faster is not just stylistic, but also helps to avoid this flicker
mass: 1, mass: 80,
stiffness: 1601.2499609375, stiffness: 100,
damping: 80, damping: 1,
); );
} }

View File

@@ -0,0 +1,58 @@
import 'remote_asset.entity.dart';
import 'local_asset.entity.dart';
-- TRIGGERS ON local_asset_entity
-- Find and update the remote_id in local_asset_entity and local_id in remote_asset_entity when checksum is set
CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_checksum_set_ids
AFTER UPDATE OF checksum ON local_asset_entity
FOR EACH ROW
WHEN NEW.checksum IS NOT NULL
BEGIN
UPDATE local_asset_entity
SET remote_id = (SELECT id FROM remote_asset_entity WHERE checksum = NEW.checksum LIMIT 1)
WHERE id = NEW.id;
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1)
WHERE checksum = NEW.checksum;
END;
-- When a local asset is updated, relink remote assets that had a checksum match
CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_old_checksum_set_remote_asset_local_id
AFTER UPDATE OF checksum ON local_asset_entity
FOR EACH ROW
WHEN OLD.checksum IS NOT NULL
BEGIN
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1)
WHERE checksum = OLD.checksum;
END;
-- remote_asset_entity.checksum is a 1..* relationship with local_asset_entity.checksum.
-- When a local asset is deleted, update remote assets that had a checksum match
-- to ensure their local_id is set to the first matching local asset or NULL
CREATE TRIGGER IF NOT EXISTS tr_local_asset_delete_update_remote_asset_local_id
AFTER DELETE ON local_asset_entity
FOR EACH ROW
WHEN OLD.checksum IS NOT NULL
BEGIN
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1)
WHERE checksum = OLD.checksum;
END;
-- TRIGGERS ON remote_asset_entity
-- Find and update local_id in remote_asset_entity when a new remote asset is inserted
CREATE TRIGGER IF NOT EXISTS tr_remote_asset_insert_set_local_id
AFTER INSERT ON remote_asset_entity
FOR EACH ROW
BEGIN
UPDATE remote_asset_entity
SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1)
WHERE id = NEW.id;
UPDATE local_asset_entity SET remote_id = NEW.id WHERE checksum = NEW.checksum;
END;

View File

@@ -0,0 +1,16 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
i0.Trigger get trLocalAssetUpdateChecksumSetIds => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_checksum_set_ids AFTER UPDATE OF checksum ON local_asset_entity WHEN NEW.checksum IS NOT NULL BEGIN UPDATE local_asset_entity SET remote_id = (SELECT id FROM remote_asset_entity WHERE checksum = NEW.checksum LIMIT 1) WHERE id = NEW.id;UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1) WHERE checksum = NEW.checksum;END',
'tr_local_asset_update_checksum_set_ids');
i0.Trigger get trLocalAssetUpdateOldChecksumSetRemoteAssetLocalId => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_local_asset_update_old_checksum_set_remote_asset_local_id AFTER UPDATE OF checksum ON local_asset_entity WHEN OLD.checksum IS NOT NULL BEGIN UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1) WHERE checksum = OLD.checksum;END',
'tr_local_asset_update_old_checksum_set_remote_asset_local_id');
i0.Trigger get trLocalAssetDeleteUpdateRemoteAssetLocalId => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_local_asset_delete_update_remote_asset_local_id AFTER DELETE ON local_asset_entity WHEN OLD.checksum IS NOT NULL BEGIN UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = OLD.checksum ORDER BY id ASC LIMIT 1) WHERE checksum = OLD.checksum;END',
'tr_local_asset_delete_update_remote_asset_local_id');
i0.Trigger get trRemoteAssetInsertSetLocalId => i0.Trigger(
'CREATE TRIGGER IF NOT EXISTS tr_remote_asset_insert_set_local_id AFTER INSERT ON remote_asset_entity BEGIN UPDATE remote_asset_entity SET local_id = (SELECT id FROM local_asset_entity WHERE checksum = NEW.checksum ORDER BY id ASC LIMIT 1) WHERE id = NEW.id;UPDATE local_asset_entity SET remote_id = NEW.id WHERE checksum = NEW.checksum;END',
'tr_remote_asset_insert_set_local_id');

View File

@@ -1,6 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@@ -9,13 +10,16 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const LocalAssetEntity(); const LocalAssetEntity();
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get remoteId => text()
.nullable()
.references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull)();
TextColumn get checksum => text().nullable()(); TextColumn get checksum => text().nullable()();
// Only used during backup to mirror the favorite status of the asset in the server // Only used during backup to mirror the favorite status of the asset in the server
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
IntColumn get orientation => integer().withDefault(const Constant(0))();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@@ -32,7 +36,6 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
isFavorite: isFavorite, isFavorite: isFavorite,
height: height, height: height,
width: width, width: width,
remoteId: null, remoteId: remoteId,
orientation: orientation,
); );
} }

View File

@@ -7,6 +7,9 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart' import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'
as i3; as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$LocalAssetEntityTableCreateCompanionBuilder typedef $$LocalAssetEntityTableCreateCompanionBuilder
= i1.LocalAssetEntityCompanion Function({ = i1.LocalAssetEntityCompanion Function({
@@ -18,9 +21,9 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder
i0.Value<int?> height, i0.Value<int?> height,
i0.Value<int?> durationInSeconds, i0.Value<int?> durationInSeconds,
required String id, required String id,
i0.Value<String?> remoteId,
i0.Value<String?> checksum, i0.Value<String?> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<int> orientation,
}); });
typedef $$LocalAssetEntityTableUpdateCompanionBuilder typedef $$LocalAssetEntityTableUpdateCompanionBuilder
= i1.LocalAssetEntityCompanion Function({ = i1.LocalAssetEntityCompanion Function({
@@ -32,11 +35,43 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder
i0.Value<int?> height, i0.Value<int?> height,
i0.Value<int?> durationInSeconds, i0.Value<int?> durationInSeconds,
i0.Value<String> id, i0.Value<String> id,
i0.Value<String?> remoteId,
i0.Value<String?> checksum, i0.Value<String?> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<int> orientation,
}); });
final class $$LocalAssetEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase, i1.$LocalAssetEntityTable, i1.LocalAssetEntityData> {
$$LocalAssetEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$RemoteAssetEntityTable _remoteIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAssetEntityTable>('local_asset_entity')
.remoteId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.id));
i5.$$RemoteAssetEntityTableProcessedTableManager? get remoteId {
final $_column = $_itemColumn<String>('remote_id');
if ($_column == null) return null;
final manager = i5
.$$RemoteAssetEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_remoteIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$LocalAssetEntityTableFilterComposer class $$LocalAssetEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> { extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> {
$$LocalAssetEntityTableFilterComposer({ $$LocalAssetEntityTableFilterComposer({
@@ -79,9 +114,27 @@ class $$LocalAssetEntityTableFilterComposer
i0.ColumnFilters<bool> get isFavorite => $composableBuilder( i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get orientation => $composableBuilder( i5.$$RemoteAssetEntityTableFilterComposer get remoteId {
column: $table.orientation, final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
builder: (column) => i0.ColumnFilters(column)); composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
} }
class $$LocalAssetEntityTableOrderingComposer class $$LocalAssetEntityTableOrderingComposer
@@ -127,9 +180,29 @@ class $$LocalAssetEntityTableOrderingComposer
column: $table.isFavorite, column: $table.isFavorite,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get orientation => $composableBuilder( i5.$$RemoteAssetEntityTableOrderingComposer get remoteId {
column: $table.orientation, final i5.$$RemoteAssetEntityTableOrderingComposer composer =
builder: (column) => i0.ColumnOrderings(column)); $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
} }
class $$LocalAssetEntityTableAnnotationComposer class $$LocalAssetEntityTableAnnotationComposer
@@ -171,8 +244,29 @@ class $$LocalAssetEntityTableAnnotationComposer
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder( i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => column); column: $table.isFavorite, builder: (column) => column);
i0.GeneratedColumn<int> get orientation => $composableBuilder( i5.$$RemoteAssetEntityTableAnnotationComposer get remoteId {
column: $table.orientation, builder: (column) => column); final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.remoteId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$RemoteAssetEntityTable>(
'remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
} }
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
@@ -184,13 +278,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i1.$$LocalAssetEntityTableAnnotationComposer, i1.$$LocalAssetEntityTableAnnotationComposer,
$$LocalAssetEntityTableCreateCompanionBuilder, $$LocalAssetEntityTableCreateCompanionBuilder,
$$LocalAssetEntityTableUpdateCompanionBuilder, $$LocalAssetEntityTableUpdateCompanionBuilder,
( (i1.LocalAssetEntityData, i1.$$LocalAssetEntityTableReferences),
i1.LocalAssetEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData>
),
i1.LocalAssetEntityData, i1.LocalAssetEntityData,
i0.PrefetchHooks Function()> { i0.PrefetchHooks Function({bool remoteId})> {
$$LocalAssetEntityTableTableManager( $$LocalAssetEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table) i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table)
: super(i0.TableManagerState( : super(i0.TableManagerState(
@@ -212,9 +302,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(), i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(), i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(), i0.Value<String> id = const i0.Value.absent(),
i0.Value<String?> remoteId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(),
}) => }) =>
i1.LocalAssetEntityCompanion( i1.LocalAssetEntityCompanion(
name: name, name: name,
@@ -225,9 +315,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
id: id, id: id,
remoteId: remoteId,
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
required String name, required String name,
@@ -238,9 +328,9 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(), i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(), i0.Value<int?> durationInSeconds = const i0.Value.absent(),
required String id, required String id,
i0.Value<String?> remoteId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(),
}) => }) =>
i1.LocalAssetEntityCompanion.insert( i1.LocalAssetEntityCompanion.insert(
name: name, name: name,
@@ -251,14 +341,52 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
id: id, id: id,
remoteId: remoteId,
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) .map((e) => (
e.readTable(table),
i1.$$LocalAssetEntityTableReferences(db, table, e)
))
.toList(), .toList(),
prefetchHooksCallback: null, prefetchHooksCallback: ({remoteId = 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 (remoteId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.remoteId,
referencedTable:
i1.$$LocalAssetEntityTableReferences._remoteIdTable(db),
referencedColumn: i1.$$LocalAssetEntityTableReferences
._remoteIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
)); ));
} }
@@ -271,13 +399,9 @@ typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.$$LocalAssetEntityTableAnnotationComposer, i1.$$LocalAssetEntityTableAnnotationComposer,
$$LocalAssetEntityTableCreateCompanionBuilder, $$LocalAssetEntityTableCreateCompanionBuilder,
$$LocalAssetEntityTableUpdateCompanionBuilder, $$LocalAssetEntityTableUpdateCompanionBuilder,
( (i1.LocalAssetEntityData, i1.$$LocalAssetEntityTableReferences),
i1.LocalAssetEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData>
),
i1.LocalAssetEntityData, i1.LocalAssetEntityData,
i0.PrefetchHooks Function()>; i0.PrefetchHooks Function({bool remoteId})>;
i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum', i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum',
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
@@ -338,6 +462,15 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>( late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false, 'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true); type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _remoteIdMeta =
const i0.VerificationMeta('remoteId');
@override
late final i0.GeneratedColumn<String> remoteId = i0.GeneratedColumn<String>(
'remote_id', aliasedName, true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
static const i0.VerificationMeta _checksumMeta = static const i0.VerificationMeta _checksumMeta =
const i0.VerificationMeta('checksum'); const i0.VerificationMeta('checksum');
@override @override
@@ -354,14 +487,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
defaultConstraints: i0.GeneratedColumn.constraintIsAlways( defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_favorite" IN (0, 1))'), 'CHECK ("is_favorite" IN (0, 1))'),
defaultValue: const i4.Constant(false)); defaultValue: const i4.Constant(false));
static const i0.VerificationMeta _orientationMeta =
const i0.VerificationMeta('orientation');
@override
late final i0.GeneratedColumn<int> orientation = i0.GeneratedColumn<int>(
'orientation', aliasedName, false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0));
@override @override
List<i0.GeneratedColumn> get $columns => [ List<i0.GeneratedColumn> get $columns => [
name, name,
@@ -372,9 +497,9 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
height, height,
durationInSeconds, durationInSeconds,
id, id,
remoteId,
checksum, checksum,
isFavorite, isFavorite
orientation
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -420,6 +545,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
} else if (isInserting) { } else if (isInserting) {
context.missing(_idMeta); context.missing(_idMeta);
} }
if (data.containsKey('remote_id')) {
context.handle(_remoteIdMeta,
remoteId.isAcceptableOrUnknown(data['remote_id']!, _remoteIdMeta));
}
if (data.containsKey('checksum')) { if (data.containsKey('checksum')) {
context.handle(_checksumMeta, context.handle(_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta));
@@ -430,12 +559,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
isFavorite.isAcceptableOrUnknown( isFavorite.isAcceptableOrUnknown(
data['is_favorite']!, _isFavoriteMeta)); data['is_favorite']!, _isFavoriteMeta));
} }
if (data.containsKey('orientation')) {
context.handle(
_orientationMeta,
orientation.isAcceptableOrUnknown(
data['orientation']!, _orientationMeta));
}
return context; return context;
} }
@@ -463,12 +586,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']),
id: attachedDatabase.typeMapping id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
remoteId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}remote_id']),
checksum: attachedDatabase.typeMapping checksum: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']),
isFavorite: attachedDatabase.typeMapping isFavorite: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
orientation: attachedDatabase.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}orientation'])!,
); );
} }
@@ -495,9 +618,9 @@ class LocalAssetEntityData extends i0.DataClass
final int? height; final int? height;
final int? durationInSeconds; final int? durationInSeconds;
final String id; final String id;
final String? remoteId;
final String? checksum; final String? checksum;
final bool isFavorite; final bool isFavorite;
final int orientation;
const LocalAssetEntityData( const LocalAssetEntityData(
{required this.name, {required this.name,
required this.type, required this.type,
@@ -507,9 +630,9 @@ class LocalAssetEntityData extends i0.DataClass
this.height, this.height,
this.durationInSeconds, this.durationInSeconds,
required this.id, required this.id,
this.remoteId,
this.checksum, this.checksum,
required this.isFavorite, required this.isFavorite});
required this.orientation});
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{}; final map = <String, i0.Expression>{};
@@ -530,11 +653,13 @@ class LocalAssetEntityData extends i0.DataClass
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds); map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
} }
map['id'] = i0.Variable<String>(id); map['id'] = i0.Variable<String>(id);
if (!nullToAbsent || remoteId != null) {
map['remote_id'] = i0.Variable<String>(remoteId);
}
if (!nullToAbsent || checksum != null) { if (!nullToAbsent || checksum != null) {
map['checksum'] = i0.Variable<String>(checksum); map['checksum'] = i0.Variable<String>(checksum);
} }
map['is_favorite'] = i0.Variable<bool>(isFavorite); map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['orientation'] = i0.Variable<int>(orientation);
return map; return map;
} }
@@ -551,9 +676,9 @@ class LocalAssetEntityData extends i0.DataClass
height: serializer.fromJson<int?>(json['height']), height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']), durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
remoteId: serializer.fromJson<String?>(json['remoteId']),
checksum: serializer.fromJson<String?>(json['checksum']), checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']), isFavorite: serializer.fromJson<bool>(json['isFavorite']),
orientation: serializer.fromJson<int>(json['orientation']),
); );
} }
@override @override
@@ -569,9 +694,9 @@ class LocalAssetEntityData extends i0.DataClass
'height': serializer.toJson<int?>(height), 'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds), 'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'remoteId': serializer.toJson<String?>(remoteId),
'checksum': serializer.toJson<String?>(checksum), 'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite), 'isFavorite': serializer.toJson<bool>(isFavorite),
'orientation': serializer.toJson<int>(orientation),
}; };
} }
@@ -584,9 +709,9 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<int?> height = const i0.Value.absent(), i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(), i0.Value<int?> durationInSeconds = const i0.Value.absent(),
String? id, String? id,
i0.Value<String?> remoteId = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite, bool? isFavorite}) =>
int? orientation}) =>
i1.LocalAssetEntityData( i1.LocalAssetEntityData(
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
@@ -598,9 +723,9 @@ class LocalAssetEntityData extends i0.DataClass
? durationInSeconds.value ? durationInSeconds.value
: this.durationInSeconds, : this.durationInSeconds,
id: id ?? this.id, id: id ?? this.id,
remoteId: remoteId.present ? remoteId.value : this.remoteId,
checksum: checksum.present ? checksum.value : this.checksum, checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
); );
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData( return LocalAssetEntityData(
@@ -614,11 +739,10 @@ class LocalAssetEntityData extends i0.DataClass
? data.durationInSeconds.value ? data.durationInSeconds.value
: this.durationInSeconds, : this.durationInSeconds,
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
remoteId: data.remoteId.present ? data.remoteId.value : this.remoteId,
checksum: data.checksum.present ? data.checksum.value : this.checksum, checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite: isFavorite:
data.isFavorite.present ? data.isFavorite.value : this.isFavorite, data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
orientation:
data.orientation.present ? data.orientation.value : this.orientation,
); );
} }
@@ -633,16 +757,16 @@ class LocalAssetEntityData extends i0.DataClass
..write('height: $height, ') ..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ') ..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ') ..write('id: $id, ')
..write('remoteId: $remoteId, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite')
..write('orientation: $orientation')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, int get hashCode => Object.hash(name, type, createdAt, updatedAt, width,
height, durationInSeconds, id, checksum, isFavorite, orientation); height, durationInSeconds, id, remoteId, checksum, isFavorite);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -655,9 +779,9 @@ class LocalAssetEntityData extends i0.DataClass
other.height == this.height && other.height == this.height &&
other.durationInSeconds == this.durationInSeconds && other.durationInSeconds == this.durationInSeconds &&
other.id == this.id && other.id == this.id &&
other.remoteId == this.remoteId &&
other.checksum == this.checksum && other.checksum == this.checksum &&
other.isFavorite == this.isFavorite && other.isFavorite == this.isFavorite);
other.orientation == this.orientation);
} }
class LocalAssetEntityCompanion class LocalAssetEntityCompanion
@@ -670,9 +794,9 @@ class LocalAssetEntityCompanion
final i0.Value<int?> height; final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds; final i0.Value<int?> durationInSeconds;
final i0.Value<String> id; final i0.Value<String> id;
final i0.Value<String?> remoteId;
final i0.Value<String?> checksum; final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite; final i0.Value<bool> isFavorite;
final i0.Value<int> orientation;
const LocalAssetEntityCompanion({ const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(), this.type = const i0.Value.absent(),
@@ -682,9 +806,9 @@ class LocalAssetEntityCompanion
this.height = const i0.Value.absent(), this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(),
this.id = const i0.Value.absent(), this.id = const i0.Value.absent(),
this.remoteId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
}); });
LocalAssetEntityCompanion.insert({ LocalAssetEntityCompanion.insert({
required String name, required String name,
@@ -695,9 +819,9 @@ class LocalAssetEntityCompanion
this.height = const i0.Value.absent(), this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(),
required String id, required String id,
this.remoteId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(),
}) : name = i0.Value(name), }) : name = i0.Value(name),
type = i0.Value(type), type = i0.Value(type),
id = i0.Value(id); id = i0.Value(id);
@@ -710,9 +834,9 @@ class LocalAssetEntityCompanion
i0.Expression<int>? height, i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds, i0.Expression<int>? durationInSeconds,
i0.Expression<String>? id, i0.Expression<String>? id,
i0.Expression<String>? remoteId,
i0.Expression<String>? checksum, i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite, i0.Expression<bool>? isFavorite,
i0.Expression<int>? orientation,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (name != null) 'name': name, if (name != null) 'name': name,
@@ -723,9 +847,9 @@ class LocalAssetEntityCompanion
if (height != null) 'height': height, if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (id != null) 'id': id, if (id != null) 'id': id,
if (remoteId != null) 'remote_id': remoteId,
if (checksum != null) 'checksum': checksum, if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite, if (isFavorite != null) 'is_favorite': isFavorite,
if (orientation != null) 'orientation': orientation,
}); });
} }
@@ -738,9 +862,9 @@ class LocalAssetEntityCompanion
i0.Value<int?>? height, i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds, i0.Value<int?>? durationInSeconds,
i0.Value<String>? id, i0.Value<String>? id,
i0.Value<String?>? remoteId,
i0.Value<String?>? checksum, i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite, i0.Value<bool>? isFavorite}) {
i0.Value<int>? orientation}) {
return i1.LocalAssetEntityCompanion( return i1.LocalAssetEntityCompanion(
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
@@ -750,9 +874,9 @@ class LocalAssetEntityCompanion
height: height ?? this.height, height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds, durationInSeconds: durationInSeconds ?? this.durationInSeconds,
id: id ?? this.id, id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
); );
} }
@@ -784,15 +908,15 @@ class LocalAssetEntityCompanion
if (id.present) { if (id.present) {
map['id'] = i0.Variable<String>(id.value); map['id'] = i0.Variable<String>(id.value);
} }
if (remoteId.present) {
map['remote_id'] = i0.Variable<String>(remoteId.value);
}
if (checksum.present) { if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value); map['checksum'] = i0.Variable<String>(checksum.value);
} }
if (isFavorite.present) { if (isFavorite.present) {
map['is_favorite'] = i0.Variable<bool>(isFavorite.value); map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
} }
if (orientation.present) {
map['orientation'] = i0.Variable<int>(orientation.value);
}
return map; return map;
} }
@@ -807,9 +931,9 @@ class LocalAssetEntityCompanion
..write('height: $height, ') ..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ') ..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ') ..write('id: $id, ')
..write('remoteId: $remoteId, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite')
..write('orientation: $orientation')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@@ -5,7 +5,7 @@ mergedAsset: SELECT * FROM
( (
SELECT SELECT
rae.id as remote_id, rae.id as remote_id,
lae.id as local_id, rae.local_id as local_id,
rae.name, rae.name,
rae."type", rae."type",
rae.created_at, rae.created_at,
@@ -16,18 +16,14 @@ mergedAsset: SELECT * FROM
rae.is_favorite, rae.is_favorite,
rae.thumb_hash, rae.thumb_hash,
rae.checksum, rae.checksum,
rae.owner_id, rae.owner_id
rae.live_photo_video_id,
0 as orientation
FROM FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum
WHERE WHERE
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ? rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
UNION ALL UNION ALL
SELECT SELECT
NULL as remote_id, lae.remote_id as remote_id,
lae.id as local_id, lae.id as local_id,
lae.name, lae.name,
lae."type", lae."type",
@@ -39,15 +35,11 @@ mergedAsset: SELECT * FROM
lae.is_favorite, lae.is_favorite,
NULL as thumb_hash, NULL as thumb_hash,
lae.checksum, lae.checksum,
NULL as owner_id, NULL as owner_id
NULL as live_photo_video_id,
lae.orientation
FROM FROM
local_asset_entity lae local_asset_entity lae
LEFT JOIN
remote_asset_entity rae ON rae.checksum = lae.checksum
WHERE WHERE
rae.id IS NULL lae.remote_id IS NULL
) )
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT $limit; LIMIT $limit;
@@ -66,8 +58,6 @@ FROM
rae.created_at rae.created_at
FROM FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN
local_asset_entity lae ON rae.checksum = lae.checksum
WHERE WHERE
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ? rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
UNION ALL UNION ALL
@@ -76,10 +66,8 @@ FROM
lae.created_at lae.created_at
FROM FROM
local_asset_entity lae local_asset_entity lae
LEFT JOIN
remote_asset_entity rae ON rae.checksum = lae.checksum
WHERE WHERE
rae.id IS NULL lae.remote_id IS NULL
) )
GROUP BY bucket_date GROUP BY bucket_date
ORDER BY bucket_date DESC; ORDER BY bucket_date DESC;

View File

@@ -18,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final generatedlimit = $write(limit, startIndex: $arrayStartIndex); final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedlimit.amountOfVariables; $arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect( return customSelect(
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.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 FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.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 FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}', 'SELECT * FROM (SELECT rae.id AS remote_id, rae.local_id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT lae.remote_id AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.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 FROM local_asset_entity AS lae WHERE lae.remote_id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [ variables: [
for (var $ in var1) i0.Variable<String>($), for (var $ in var1) i0.Variable<String>($),
...generatedlimit.introducedVariables ...generatedlimit.introducedVariables
@@ -42,8 +42,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
thumbHash: row.readNullable<String>('thumb_hash'), thumbHash: row.readNullable<String>('thumb_hash'),
checksum: row.readNullable<String>('checksum'), checksum: row.readNullable<String>('checksum'),
ownerId: row.readNullable<String>('owner_id'), ownerId: row.readNullable<String>('owner_id'),
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'),
)); ));
} }
@@ -53,7 +51,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final expandedvar2 = $expandVar($arrayStartIndex, var2.length); final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
$arrayStartIndex += var2.length; $arrayStartIndex += var2.length;
return customSelect( return customSelect(
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC', 'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae WHERE lae.remote_id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
variables: [ variables: [
i0.Variable<int>(groupBy), i0.Variable<int>(groupBy),
for (var $ in var2) i0.Variable<String>($) for (var $ in var2) i0.Variable<String>($)
@@ -89,8 +87,6 @@ class MergedAssetResult {
final String? thumbHash; final String? thumbHash;
final String? checksum; final String? checksum;
final String? ownerId; final String? ownerId;
final String? livePhotoVideoId;
final int orientation;
MergedAssetResult({ MergedAssetResult({
this.remoteId, this.remoteId,
this.localId, this.localId,
@@ -105,8 +101,6 @@ class MergedAssetResult {
this.thumbHash, this.thumbHash,
this.checksum, this.checksum,
this.ownerId, this.ownerId,
this.livePhotoVideoId,
required this.orientation,
}); });
} }

View File

@@ -17,6 +17,8 @@ class RemoteAssetEntity extends Table
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get localId => text().nullable()();
TextColumn get checksum => text()(); TextColumn get checksum => text()();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
@@ -30,8 +32,6 @@ class RemoteAssetEntity extends Table
DateTimeColumn get deletedAt => dateTime().nullable()(); DateTimeColumn get deletedAt => dateTime().nullable()();
TextColumn get livePhotoVideoId => text().nullable()();
IntColumn get visibility => intEnum<AssetVisibility>()(); IntColumn get visibility => intEnum<AssetVisibility>()();
@override @override
@@ -53,7 +53,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
width: width, width: width,
thumbHash: thumbHash, thumbHash: thumbHash,
visibility: visibility, visibility: visibility,
livePhotoVideoId: livePhotoVideoId, localId: localId,
localId: null,
); );
} }

View File

@@ -21,13 +21,13 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
i0.Value<int?> height, i0.Value<int?> height,
i0.Value<int?> durationInSeconds, i0.Value<int?> durationInSeconds,
required String id, required String id,
i0.Value<String?> localId,
required String checksum, required String checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
required String ownerId, required String ownerId,
i0.Value<DateTime?> localDateTime, i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash, i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt, i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId,
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
}); });
typedef $$RemoteAssetEntityTableUpdateCompanionBuilder typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
@@ -40,13 +40,13 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
i0.Value<int?> height, i0.Value<int?> height,
i0.Value<int?> durationInSeconds, i0.Value<int?> durationInSeconds,
i0.Value<String> id, i0.Value<String> id,
i0.Value<String?> localId,
i0.Value<String> checksum, i0.Value<String> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<String> ownerId, i0.Value<String> ownerId,
i0.Value<DateTime?> localDateTime, i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash, i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt, i0.Value<DateTime?> deletedAt,
i0.Value<String?> livePhotoVideoId,
i0.Value<i2.AssetVisibility> visibility, i0.Value<i2.AssetVisibility> visibility,
}); });
@@ -120,6 +120,9 @@ class $$RemoteAssetEntityTableFilterComposer
i0.ColumnFilters<String> get id => $composableBuilder( i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column)); column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get localId => $composableBuilder(
column: $table.localId, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get checksum => $composableBuilder( i0.ColumnFilters<String> get checksum => $composableBuilder(
column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); column: $table.checksum, builder: (column) => i0.ColumnFilters(column));
@@ -136,10 +139,6 @@ class $$RemoteAssetEntityTableFilterComposer
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder( i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.AssetVisibility, i2.AssetVisibility, int> i0.ColumnWithTypeConverterFilters<i2.AssetVisibility, i2.AssetVisibility, int>
get visibility => $composableBuilder( get visibility => $composableBuilder(
column: $table.visibility, column: $table.visibility,
@@ -204,6 +203,9 @@ class $$RemoteAssetEntityTableOrderingComposer
i0.ColumnOrderings<String> get id => $composableBuilder( i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column)); column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get localId => $composableBuilder(
column: $table.localId, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get checksum => $composableBuilder( i0.ColumnOrderings<String> get checksum => $composableBuilder(
column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); column: $table.checksum, builder: (column) => i0.ColumnOrderings(column));
@@ -223,10 +225,6 @@ class $$RemoteAssetEntityTableOrderingComposer
column: $table.deletedAt, column: $table.deletedAt,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get visibility => $composableBuilder( i0.ColumnOrderings<int> get visibility => $composableBuilder(
column: $table.visibility, column: $table.visibility,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
@@ -287,6 +285,9 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<String> get id => i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column); $composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get localId =>
$composableBuilder(column: $table.localId, builder: (column) => column);
i0.GeneratedColumn<String> get checksum => i0.GeneratedColumn<String> get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column); $composableBuilder(column: $table.checksum, builder: (column) => column);
@@ -302,9 +303,6 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<DateTime> get deletedAt => i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column); $composableBuilder(column: $table.deletedAt, builder: (column) => column);
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> get visibility => i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> get visibility =>
$composableBuilder( $composableBuilder(
column: $table.visibility, builder: (column) => column); column: $table.visibility, builder: (column) => column);
@@ -365,13 +363,13 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(), i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(), i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(), i0.Value<String> id = const i0.Value.absent(),
i0.Value<String?> localId = const i0.Value.absent(),
i0.Value<String> checksum = const i0.Value.absent(), i0.Value<String> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<String> ownerId = const i0.Value.absent(), i0.Value<String> ownerId = const i0.Value.absent(),
i0.Value<DateTime?> localDateTime = const i0.Value.absent(), i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(), i0.Value<i2.AssetVisibility> visibility = const i0.Value.absent(),
}) => }) =>
i1.RemoteAssetEntityCompanion( i1.RemoteAssetEntityCompanion(
@@ -383,13 +381,13 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
id: id, id: id,
localId: localId,
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
ownerId: ownerId, ownerId: ownerId,
localDateTime: localDateTime, localDateTime: localDateTime,
thumbHash: thumbHash, thumbHash: thumbHash,
deletedAt: deletedAt, deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility, visibility: visibility,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
@@ -401,13 +399,13 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
i0.Value<int?> height = const i0.Value.absent(), i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(), i0.Value<int?> durationInSeconds = const i0.Value.absent(),
required String id, required String id,
i0.Value<String?> localId = const i0.Value.absent(),
required String checksum, required String checksum,
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
required String ownerId, required String ownerId,
i0.Value<DateTime?> localDateTime = const i0.Value.absent(), i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
}) => }) =>
i1.RemoteAssetEntityCompanion.insert( i1.RemoteAssetEntityCompanion.insert(
@@ -419,13 +417,13 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
id: id, id: id,
localId: localId,
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
ownerId: ownerId, ownerId: ownerId,
localDateTime: localDateTime, localDateTime: localDateTime,
thumbHash: thumbHash, thumbHash: thumbHash,
deletedAt: deletedAt, deletedAt: deletedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility, visibility: visibility,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
@@ -547,6 +545,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>( late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false, 'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true); type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _localIdMeta =
const i0.VerificationMeta('localId');
@override
late final i0.GeneratedColumn<String> localId = i0.GeneratedColumn<String>(
'local_id', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _checksumMeta = static const i0.VerificationMeta _checksumMeta =
const i0.VerificationMeta('checksum'); const i0.VerificationMeta('checksum');
@override @override
@@ -590,12 +594,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
late final i0.GeneratedColumn<DateTime> deletedAt = late final i0.GeneratedColumn<DateTime> deletedAt =
i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true, i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
static const i0.VerificationMeta _livePhotoVideoIdMeta =
const i0.VerificationMeta('livePhotoVideoId');
@override
late final i0.GeneratedColumn<String> livePhotoVideoId =
i0.GeneratedColumn<String>('live_photo_video_id', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
@override @override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int> late final i0.GeneratedColumnWithTypeConverter<i2.AssetVisibility, int>
visibility = i0.GeneratedColumn<int>('visibility', aliasedName, false, visibility = i0.GeneratedColumn<int>('visibility', aliasedName, false,
@@ -612,13 +610,13 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
height, height,
durationInSeconds, durationInSeconds,
id, id,
localId,
checksum, checksum,
isFavorite, isFavorite,
ownerId, ownerId,
localDateTime, localDateTime,
thumbHash, thumbHash,
deletedAt, deletedAt,
livePhotoVideoId,
visibility visibility
]; ];
@override @override
@@ -665,6 +663,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
} else if (isInserting) { } else if (isInserting) {
context.missing(_idMeta); context.missing(_idMeta);
} }
if (data.containsKey('local_id')) {
context.handle(_localIdMeta,
localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta));
}
if (data.containsKey('checksum')) { if (data.containsKey('checksum')) {
context.handle(_checksumMeta, context.handle(_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta));
@@ -697,12 +699,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
context.handle(_deletedAtMeta, context.handle(_deletedAtMeta,
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta));
} }
if (data.containsKey('live_photo_video_id')) {
context.handle(
_livePhotoVideoIdMeta,
livePhotoVideoId.isAcceptableOrUnknown(
data['live_photo_video_id']!, _livePhotoVideoIdMeta));
}
return context; return context;
} }
@@ -730,6 +726,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']),
id: attachedDatabase.typeMapping id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
localId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}local_id']),
checksum: attachedDatabase.typeMapping checksum: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!, .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!,
isFavorite: attachedDatabase.typeMapping isFavorite: attachedDatabase.typeMapping
@@ -742,9 +740,6 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
.read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']), .read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']),
deletedAt: attachedDatabase.typeMapping deletedAt: attachedDatabase.typeMapping
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']),
livePhotoVideoId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}live_photo_video_id']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql(
attachedDatabase.typeMapping.read( attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!),
@@ -777,13 +772,13 @@ class RemoteAssetEntityData extends i0.DataClass
final int? height; final int? height;
final int? durationInSeconds; final int? durationInSeconds;
final String id; final String id;
final String? localId;
final String checksum; final String checksum;
final bool isFavorite; final bool isFavorite;
final String ownerId; final String ownerId;
final DateTime? localDateTime; final DateTime? localDateTime;
final String? thumbHash; final String? thumbHash;
final DateTime? deletedAt; final DateTime? deletedAt;
final String? livePhotoVideoId;
final i2.AssetVisibility visibility; final i2.AssetVisibility visibility;
const RemoteAssetEntityData( const RemoteAssetEntityData(
{required this.name, {required this.name,
@@ -794,13 +789,13 @@ class RemoteAssetEntityData extends i0.DataClass
this.height, this.height,
this.durationInSeconds, this.durationInSeconds,
required this.id, required this.id,
this.localId,
required this.checksum, required this.checksum,
required this.isFavorite, required this.isFavorite,
required this.ownerId, required this.ownerId,
this.localDateTime, this.localDateTime,
this.thumbHash, this.thumbHash,
this.deletedAt, this.deletedAt,
this.livePhotoVideoId,
required this.visibility}); required this.visibility});
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -822,6 +817,9 @@ class RemoteAssetEntityData extends i0.DataClass
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds); map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
} }
map['id'] = i0.Variable<String>(id); map['id'] = i0.Variable<String>(id);
if (!nullToAbsent || localId != null) {
map['local_id'] = i0.Variable<String>(localId);
}
map['checksum'] = i0.Variable<String>(checksum); map['checksum'] = i0.Variable<String>(checksum);
map['is_favorite'] = i0.Variable<bool>(isFavorite); map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['owner_id'] = i0.Variable<String>(ownerId); map['owner_id'] = i0.Variable<String>(ownerId);
@@ -834,9 +832,6 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || deletedAt != null) { if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt); map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
} }
if (!nullToAbsent || livePhotoVideoId != null) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
}
{ {
map['visibility'] = i0.Variable<int>( map['visibility'] = i0.Variable<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility));
@@ -857,13 +852,13 @@ class RemoteAssetEntityData extends i0.DataClass
height: serializer.fromJson<int?>(json['height']), height: serializer.fromJson<int?>(json['height']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']), durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
localId: serializer.fromJson<String?>(json['localId']),
checksum: serializer.fromJson<String>(json['checksum']), checksum: serializer.fromJson<String>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']), isFavorite: serializer.fromJson<bool>(json['isFavorite']),
ownerId: serializer.fromJson<String>(json['ownerId']), ownerId: serializer.fromJson<String>(json['ownerId']),
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']), localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
thumbHash: serializer.fromJson<String?>(json['thumbHash']), thumbHash: serializer.fromJson<String?>(json['thumbHash']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']), deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility visibility: i1.$RemoteAssetEntityTable.$convertervisibility
.fromJson(serializer.fromJson<int>(json['visibility'])), .fromJson(serializer.fromJson<int>(json['visibility'])),
); );
@@ -881,13 +876,13 @@ class RemoteAssetEntityData extends i0.DataClass
'height': serializer.toJson<int?>(height), 'height': serializer.toJson<int?>(height),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds), 'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'localId': serializer.toJson<String?>(localId),
'checksum': serializer.toJson<String>(checksum), 'checksum': serializer.toJson<String>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite), 'isFavorite': serializer.toJson<bool>(isFavorite),
'ownerId': serializer.toJson<String>(ownerId), 'ownerId': serializer.toJson<String>(ownerId),
'localDateTime': serializer.toJson<DateTime?>(localDateTime), 'localDateTime': serializer.toJson<DateTime?>(localDateTime),
'thumbHash': serializer.toJson<String?>(thumbHash), 'thumbHash': serializer.toJson<String?>(thumbHash),
'deletedAt': serializer.toJson<DateTime?>(deletedAt), 'deletedAt': serializer.toJson<DateTime?>(deletedAt),
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
'visibility': serializer.toJson<int>( 'visibility': serializer.toJson<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)),
}; };
@@ -902,13 +897,13 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<int?> height = const i0.Value.absent(), i0.Value<int?> height = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(), i0.Value<int?> durationInSeconds = const i0.Value.absent(),
String? id, String? id,
i0.Value<String?> localId = const i0.Value.absent(),
String? checksum, String? checksum,
bool? isFavorite, bool? isFavorite,
String? ownerId, String? ownerId,
i0.Value<DateTime?> localDateTime = const i0.Value.absent(), i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(), i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(), i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i2.AssetVisibility? visibility}) => i2.AssetVisibility? visibility}) =>
i1.RemoteAssetEntityData( i1.RemoteAssetEntityData(
name: name ?? this.name, name: name ?? this.name,
@@ -921,6 +916,7 @@ class RemoteAssetEntityData extends i0.DataClass
? durationInSeconds.value ? durationInSeconds.value
: this.durationInSeconds, : this.durationInSeconds,
id: id ?? this.id, id: id ?? this.id,
localId: localId.present ? localId.value : this.localId,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
ownerId: ownerId ?? this.ownerId, ownerId: ownerId ?? this.ownerId,
@@ -928,9 +924,6 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime.present ? localDateTime.value : this.localDateTime, localDateTime.present ? localDateTime.value : this.localDateTime,
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
livePhotoVideoId: livePhotoVideoId.present
? livePhotoVideoId.value
: this.livePhotoVideoId,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
); );
RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) {
@@ -945,6 +938,7 @@ class RemoteAssetEntityData extends i0.DataClass
? data.durationInSeconds.value ? data.durationInSeconds.value
: this.durationInSeconds, : this.durationInSeconds,
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
localId: data.localId.present ? data.localId.value : this.localId,
checksum: data.checksum.present ? data.checksum.value : this.checksum, checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite: isFavorite:
data.isFavorite.present ? data.isFavorite.value : this.isFavorite, data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
@@ -954,9 +948,6 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime, : this.localDateTime,
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
livePhotoVideoId: data.livePhotoVideoId.present
? data.livePhotoVideoId.value
: this.livePhotoVideoId,
visibility: visibility:
data.visibility.present ? data.visibility.value : this.visibility, data.visibility.present ? data.visibility.value : this.visibility,
); );
@@ -973,13 +964,13 @@ class RemoteAssetEntityData extends i0.DataClass
..write('height: $height, ') ..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ') ..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ') ..write('id: $id, ')
..write('localId: $localId, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite, ')
..write('ownerId: $ownerId, ') ..write('ownerId: $ownerId, ')
..write('localDateTime: $localDateTime, ') ..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ') ..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ') ..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility') ..write('visibility: $visibility')
..write(')')) ..write(')'))
.toString(); .toString();
@@ -995,13 +986,13 @@ class RemoteAssetEntityData extends i0.DataClass
height, height,
durationInSeconds, durationInSeconds,
id, id,
localId,
checksum, checksum,
isFavorite, isFavorite,
ownerId, ownerId,
localDateTime, localDateTime,
thumbHash, thumbHash,
deletedAt, deletedAt,
livePhotoVideoId,
visibility); visibility);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -1015,13 +1006,13 @@ class RemoteAssetEntityData extends i0.DataClass
other.height == this.height && other.height == this.height &&
other.durationInSeconds == this.durationInSeconds && other.durationInSeconds == this.durationInSeconds &&
other.id == this.id && other.id == this.id &&
other.localId == this.localId &&
other.checksum == this.checksum && other.checksum == this.checksum &&
other.isFavorite == this.isFavorite && other.isFavorite == this.isFavorite &&
other.ownerId == this.ownerId && other.ownerId == this.ownerId &&
other.localDateTime == this.localDateTime && other.localDateTime == this.localDateTime &&
other.thumbHash == this.thumbHash && other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt && other.deletedAt == this.deletedAt &&
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility); other.visibility == this.visibility);
} }
@@ -1035,13 +1026,13 @@ class RemoteAssetEntityCompanion
final i0.Value<int?> height; final i0.Value<int?> height;
final i0.Value<int?> durationInSeconds; final i0.Value<int?> durationInSeconds;
final i0.Value<String> id; final i0.Value<String> id;
final i0.Value<String?> localId;
final i0.Value<String> checksum; final i0.Value<String> checksum;
final i0.Value<bool> isFavorite; final i0.Value<bool> isFavorite;
final i0.Value<String> ownerId; final i0.Value<String> ownerId;
final i0.Value<DateTime?> localDateTime; final i0.Value<DateTime?> localDateTime;
final i0.Value<String?> thumbHash; final i0.Value<String?> thumbHash;
final i0.Value<DateTime?> deletedAt; final i0.Value<DateTime?> deletedAt;
final i0.Value<String?> livePhotoVideoId;
final i0.Value<i2.AssetVisibility> visibility; final i0.Value<i2.AssetVisibility> visibility;
const RemoteAssetEntityCompanion({ const RemoteAssetEntityCompanion({
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
@@ -1052,13 +1043,13 @@ class RemoteAssetEntityCompanion
this.height = const i0.Value.absent(), this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(),
this.id = const i0.Value.absent(), this.id = const i0.Value.absent(),
this.localId = const i0.Value.absent(),
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.ownerId = const i0.Value.absent(), this.ownerId = const i0.Value.absent(),
this.localDateTime = const i0.Value.absent(), this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(), this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(), this.visibility = const i0.Value.absent(),
}); });
RemoteAssetEntityCompanion.insert({ RemoteAssetEntityCompanion.insert({
@@ -1070,13 +1061,13 @@ class RemoteAssetEntityCompanion
this.height = const i0.Value.absent(), this.height = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(),
required String id, required String id,
this.localId = const i0.Value.absent(),
required String checksum, required String checksum,
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
required String ownerId, required String ownerId,
this.localDateTime = const i0.Value.absent(), this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(), this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(), this.deletedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility, required i2.AssetVisibility visibility,
}) : name = i0.Value(name), }) : name = i0.Value(name),
type = i0.Value(type), type = i0.Value(type),
@@ -1093,13 +1084,13 @@ class RemoteAssetEntityCompanion
i0.Expression<int>? height, i0.Expression<int>? height,
i0.Expression<int>? durationInSeconds, i0.Expression<int>? durationInSeconds,
i0.Expression<String>? id, i0.Expression<String>? id,
i0.Expression<String>? localId,
i0.Expression<String>? checksum, i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite, i0.Expression<bool>? isFavorite,
i0.Expression<String>? ownerId, i0.Expression<String>? ownerId,
i0.Expression<DateTime>? localDateTime, i0.Expression<DateTime>? localDateTime,
i0.Expression<String>? thumbHash, i0.Expression<String>? thumbHash,
i0.Expression<DateTime>? deletedAt, i0.Expression<DateTime>? deletedAt,
i0.Expression<String>? livePhotoVideoId,
i0.Expression<int>? visibility, i0.Expression<int>? visibility,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
@@ -1111,13 +1102,13 @@ class RemoteAssetEntityCompanion
if (height != null) 'height': height, if (height != null) 'height': height,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (id != null) 'id': id, if (id != null) 'id': id,
if (localId != null) 'local_id': localId,
if (checksum != null) 'checksum': checksum, if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite, if (isFavorite != null) 'is_favorite': isFavorite,
if (ownerId != null) 'owner_id': ownerId, if (ownerId != null) 'owner_id': ownerId,
if (localDateTime != null) 'local_date_time': localDateTime, if (localDateTime != null) 'local_date_time': localDateTime,
if (thumbHash != null) 'thumb_hash': thumbHash, if (thumbHash != null) 'thumb_hash': thumbHash,
if (deletedAt != null) 'deleted_at': deletedAt, if (deletedAt != null) 'deleted_at': deletedAt,
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
if (visibility != null) 'visibility': visibility, if (visibility != null) 'visibility': visibility,
}); });
} }
@@ -1131,13 +1122,13 @@ class RemoteAssetEntityCompanion
i0.Value<int?>? height, i0.Value<int?>? height,
i0.Value<int?>? durationInSeconds, i0.Value<int?>? durationInSeconds,
i0.Value<String>? id, i0.Value<String>? id,
i0.Value<String?>? localId,
i0.Value<String>? checksum, i0.Value<String>? checksum,
i0.Value<bool>? isFavorite, i0.Value<bool>? isFavorite,
i0.Value<String>? ownerId, i0.Value<String>? ownerId,
i0.Value<DateTime?>? localDateTime, i0.Value<DateTime?>? localDateTime,
i0.Value<String?>? thumbHash, i0.Value<String?>? thumbHash,
i0.Value<DateTime?>? deletedAt, i0.Value<DateTime?>? deletedAt,
i0.Value<String?>? livePhotoVideoId,
i0.Value<i2.AssetVisibility>? visibility}) { i0.Value<i2.AssetVisibility>? visibility}) {
return i1.RemoteAssetEntityCompanion( return i1.RemoteAssetEntityCompanion(
name: name ?? this.name, name: name ?? this.name,
@@ -1148,13 +1139,13 @@ class RemoteAssetEntityCompanion
height: height ?? this.height, height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds, durationInSeconds: durationInSeconds ?? this.durationInSeconds,
id: id ?? this.id, id: id ?? this.id,
localId: localId ?? this.localId,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
ownerId: ownerId ?? this.ownerId, ownerId: ownerId ?? this.ownerId,
localDateTime: localDateTime ?? this.localDateTime, localDateTime: localDateTime ?? this.localDateTime,
thumbHash: thumbHash ?? this.thumbHash, thumbHash: thumbHash ?? this.thumbHash,
deletedAt: deletedAt ?? this.deletedAt, deletedAt: deletedAt ?? this.deletedAt,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
visibility: visibility ?? this.visibility, visibility: visibility ?? this.visibility,
); );
} }
@@ -1187,6 +1178,9 @@ class RemoteAssetEntityCompanion
if (id.present) { if (id.present) {
map['id'] = i0.Variable<String>(id.value); map['id'] = i0.Variable<String>(id.value);
} }
if (localId.present) {
map['local_id'] = i0.Variable<String>(localId.value);
}
if (checksum.present) { if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value); map['checksum'] = i0.Variable<String>(checksum.value);
} }
@@ -1205,9 +1199,6 @@ class RemoteAssetEntityCompanion
if (deletedAt.present) { if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value); map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
} }
if (livePhotoVideoId.present) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
}
if (visibility.present) { if (visibility.present) {
map['visibility'] = i0.Variable<int>(i1 map['visibility'] = i0.Variable<int>(i1
.$RemoteAssetEntityTable.$convertervisibility .$RemoteAssetEntityTable.$convertervisibility
@@ -1227,13 +1218,13 @@ class RemoteAssetEntityCompanion
..write('height: $height, ') ..write('height: $height, ')
..write('durationInSeconds: $durationInSeconds, ') ..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ') ..write('id: $id, ')
..write('localId: $localId, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite, ')
..write('ownerId: $ownerId, ') ..write('ownerId: $ownerId, ')
..write('localDateTime: $localDateTime, ') ..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ') ..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ') ..write('deletedAt: $deletedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility') ..write('visibility: $visibility')
..write(')')) ..write(')'))
.toString(); .toString();

View File

@@ -8,16 +8,14 @@ class UserMetadataEntity extends Table with DriftDefaultsMixin {
TextColumn get userId => TextColumn get userId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get preferences => text().map(userPreferenceConverter)();
IntColumn get key => intEnum<UserMetadataKey>()();
BlobColumn get value => blob().map(userMetadataConverter)();
@override @override
Set<Column> get primaryKey => {userId, key}; Set<Column> get primaryKey => {userId};
} }
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?> final JsonTypeConverter2<UserPreferences, String, Object?>
userMetadataConverter = TypeConverter.jsonb( userPreferenceConverter = TypeConverter.json2(
fromJson: (json) => json as Map<String, Object?>, fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
toJson: (pref) => pref.toMap(),
); );

View File

@@ -4,24 +4,21 @@ import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i1; as i1;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2; import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2;
import 'dart:typed_data' as i3;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart' import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
as i4; as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5; as i4;
import 'package:drift/internal/modular.dart' as i6; import 'package:drift/internal/modular.dart' as i5;
typedef $$UserMetadataEntityTableCreateCompanionBuilder typedef $$UserMetadataEntityTableCreateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({ = i1.UserMetadataEntityCompanion Function({
required String userId, required String userId,
required i2.UserMetadataKey key, required i2.UserPreferences preferences,
required Map<String, Object?> value,
}); });
typedef $$UserMetadataEntityTableUpdateCompanionBuilder typedef $$UserMetadataEntityTableUpdateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({ = i1.UserMetadataEntityCompanion Function({
i0.Value<String> userId, i0.Value<String> userId,
i0.Value<i2.UserMetadataKey> key, i0.Value<i2.UserPreferences> preferences,
i0.Value<Map<String, Object?>> value,
}); });
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
@@ -31,26 +28,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
$$UserMetadataEntityTableReferences( $$UserMetadataEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult); super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db) i5.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity') .resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator( .createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db) i5.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>( .resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity') 'user_metadata_entity')
.userId, .userId,
i6.ReadDatabaseContainer(db) i5.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity') .resultSet<i4.$UserEntityTable>('user_entity')
.id)); .id));
i5.$$UserEntityTableProcessedTableManager get userId { i4.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!; final $_column = $_itemColumn<String>('user_id')!;
final manager = i5 final manager = i4
.$$UserEntityTableTableManager( .$$UserEntityTableTableManager(
$_db, $_db,
i6.ReadDatabaseContainer($_db) i5.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity')) .resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column)); .filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_userIdTable($_db)); final item = $_typedResult.readTableOrNull(_userIdTable($_db));
if (item == null) return manager; if (item == null) return manager;
@@ -68,31 +65,26 @@ class $$UserMetadataEntityTableFilterComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
i0.ColumnWithTypeConverterFilters<i2.UserMetadataKey, i2.UserMetadataKey, int> i0.ColumnWithTypeConverterFilters<i2.UserPreferences, i2.UserPreferences,
get key => $composableBuilder( String>
column: $table.key, get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnWithTypeConverterFilters(column)); builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnWithTypeConverterFilters<Map<String, Object?>, Map<String, Object>, i4.$$UserEntityTableFilterComposer get userId {
i3.Uint8List> final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$UserEntityTableFilterComposer get userId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.userId, getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db) referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'), .resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: (joinBuilder, builder: (joinBuilder,
{$addJoinBuilderToRootComposer, {$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) => $removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer( i4.$$UserEntityTableFilterComposer(
$db: $db, $db: $db,
$table: i6.ReadDatabaseContainer($db) $table: i5.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'), .resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -111,26 +103,24 @@ class $$UserMetadataEntityTableOrderingComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
i0.ColumnOrderings<int> get key => $composableBuilder( i0.ColumnOrderings<String> get preferences => $composableBuilder(
column: $table.key, builder: (column) => i0.ColumnOrderings(column)); column: $table.preferences,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<i3.Uint8List> get value => $composableBuilder( i4.$$UserEntityTableOrderingComposer get userId {
column: $table.value, builder: (column) => i0.ColumnOrderings(column)); final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i5.$$UserEntityTableOrderingComposer get userId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.userId, getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db) referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'), .resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: (joinBuilder, builder: (joinBuilder,
{$addJoinBuilderToRootComposer, {$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) => $removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer( i4.$$UserEntityTableOrderingComposer(
$db: $db, $db: $db,
$table: i6.ReadDatabaseContainer($db) $table: i5.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'), .resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -149,27 +139,24 @@ class $$UserMetadataEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> get key => i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
$composableBuilder(column: $table.key, builder: (column) => column); get preferences => $composableBuilder(
column: $table.preferences, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List> i4.$$UserEntityTableAnnotationComposer get userId {
get value => final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
$composableBuilder(column: $table.value, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get userId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this, composer: this,
getCurrentColumn: (t) => t.userId, getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db) referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'), .resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id, getReferencedColumn: (t) => t.id,
builder: (joinBuilder, builder: (joinBuilder,
{$addJoinBuilderToRootComposer, {$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) => $removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer( i4.$$UserEntityTableAnnotationComposer(
$db: $db, $db: $db,
$table: i6.ReadDatabaseContainer($db) $table: i5.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'), .resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder, joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer:
@@ -206,23 +193,19 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
$db: db, $table: table), $db: db, $table: table),
updateCompanionCallback: ({ updateCompanionCallback: ({
i0.Value<String> userId = const i0.Value.absent(), i0.Value<String> userId = const i0.Value.absent(),
i0.Value<i2.UserMetadataKey> key = const i0.Value.absent(), i0.Value<i2.UserPreferences> preferences = const i0.Value.absent(),
i0.Value<Map<String, Object?>> value = const i0.Value.absent(),
}) => }) =>
i1.UserMetadataEntityCompanion( i1.UserMetadataEntityCompanion(
userId: userId, userId: userId,
key: key, preferences: preferences,
value: value,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
required String userId, required String userId,
required i2.UserMetadataKey key, required i2.UserPreferences preferences,
required Map<String, Object?> value,
}) => }) =>
i1.UserMetadataEntityCompanion.insert( i1.UserMetadataEntityCompanion.insert(
userId: userId, userId: userId,
key: key, preferences: preferences,
value: value,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map((e) => ( .map((e) => (
@@ -283,7 +266,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager
i1.UserMetadataEntityData, i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})>; i0.PrefetchHooks Function({bool userId})>;
class $UserMetadataEntityTable extends i4.UserMetadataEntity class $UserMetadataEntityTable extends i3.UserMetadataEntity
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> { with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
@override @override
final i0.GeneratedDatabase attachedDatabase; final i0.GeneratedDatabase attachedDatabase;
@@ -299,20 +282,14 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
defaultConstraints: i0.GeneratedColumn.constraintIsAlways( defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE')); 'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override @override
late final i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> key = late final i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
i0.GeneratedColumn<int>('key', aliasedName, false, preferences = i0.GeneratedColumn<String>(
type: i0.DriftSqlType.int, requiredDuringInsert: true) 'preferences', aliasedName, false,
.withConverter<i2.UserMetadataKey>( type: i0.DriftSqlType.string, requiredDuringInsert: true)
i1.$UserMetadataEntityTable.$converterkey); .withConverter<i2.UserPreferences>(
i1.$UserMetadataEntityTable.$converterpreferences);
@override @override
late final i0 List<i0.GeneratedColumn> get $columns => [userId, preferences];
.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
value = i0.GeneratedColumn<i3.Uint8List>('value', aliasedName, false,
type: i0.DriftSqlType.blob, requiredDuringInsert: true)
.withConverter<Map<String, Object?>>(
i1.$UserMetadataEntityTable.$convertervalue);
@override
List<i0.GeneratedColumn> get $columns => [userId, key, value];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@@ -334,7 +311,7 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
} }
@override @override
Set<i0.GeneratedColumn> get $primaryKey => {userId, key}; Set<i0.GeneratedColumn> get $primaryKey => {userId};
@override @override
i1.UserMetadataEntityData map(Map<String, dynamic> data, i1.UserMetadataEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) { {String? tablePrefix}) {
@@ -342,12 +319,9 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
return i1.UserMetadataEntityData( return i1.UserMetadataEntityData(
userId: attachedDatabase.typeMapping userId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!,
key: i1.$UserMetadataEntityTable.$converterkey.fromSql(attachedDatabase preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
.typeMapping attachedDatabase.typeMapping.read(
.read(i0.DriftSqlType.int, data['${effectivePrefix}key'])!), i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
value: i1.$UserMetadataEntityTable.$convertervalue.fromSql(
attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}value'])!),
); );
} }
@@ -356,11 +330,8 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
return $UserMetadataEntityTable(attachedDatabase, alias); return $UserMetadataEntityTable(attachedDatabase, alias);
} }
static i0.JsonTypeConverter2<i2.UserMetadataKey, int, int> $converterkey = static i0.JsonTypeConverter2<i2.UserPreferences, String, Object?>
const i0.EnumIndexConverter<i2.UserMetadataKey>( $converterpreferences = i3.userPreferenceConverter;
i2.UserMetadataKey.values);
static i0.JsonTypeConverter2<Map<String, Object?>, i3.Uint8List, Object?>
$convertervalue = i4.userMetadataConverter;
@override @override
bool get withoutRowId => true; bool get withoutRowId => true;
@override @override
@@ -370,21 +341,16 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
class UserMetadataEntityData extends i0.DataClass class UserMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.UserMetadataEntityData> { implements i0.Insertable<i1.UserMetadataEntityData> {
final String userId; final String userId;
final i2.UserMetadataKey key; final i2.UserPreferences preferences;
final Map<String, Object?> value;
const UserMetadataEntityData( const UserMetadataEntityData(
{required this.userId, required this.key, required this.value}); {required this.userId, required this.preferences});
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{}; final map = <String, i0.Expression>{};
map['user_id'] = i0.Variable<String>(userId); map['user_id'] = i0.Variable<String>(userId);
{ {
map['key'] = i0.Variable<int>( map['preferences'] = i0.Variable<String>(
i1.$UserMetadataEntityTable.$converterkey.toSql(key)); i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
}
{
map['value'] = i0.Variable<i3.Uint8List>(
i1.$UserMetadataEntityTable.$convertervalue.toSql(value));
} }
return map; return map;
} }
@@ -394,10 +360,8 @@ class UserMetadataEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer; serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserMetadataEntityData( return UserMetadataEntityData(
userId: serializer.fromJson<String>(json['userId']), userId: serializer.fromJson<String>(json['userId']),
key: i1.$UserMetadataEntityTable.$converterkey preferences: i1.$UserMetadataEntityTable.$converterpreferences
.fromJson(serializer.fromJson<int>(json['key'])), .fromJson(serializer.fromJson<Object?>(json['preferences'])),
value: i1.$UserMetadataEntityTable.$convertervalue
.fromJson(serializer.fromJson<Object?>(json['value'])),
); );
} }
@override @override
@@ -405,28 +369,24 @@ class UserMetadataEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer; serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'userId': serializer.toJson<String>(userId), 'userId': serializer.toJson<String>(userId),
'key': serializer 'preferences': serializer.toJson<Object?>(i1
.toJson<int>(i1.$UserMetadataEntityTable.$converterkey.toJson(key)), .$UserMetadataEntityTable.$converterpreferences
'value': serializer.toJson<Object?>( .toJson(preferences)),
i1.$UserMetadataEntityTable.$convertervalue.toJson(value)),
}; };
} }
i1.UserMetadataEntityData copyWith( i1.UserMetadataEntityData copyWith(
{String? userId, {String? userId, i2.UserPreferences? preferences}) =>
i2.UserMetadataKey? key,
Map<String, Object?>? value}) =>
i1.UserMetadataEntityData( i1.UserMetadataEntityData(
userId: userId ?? this.userId, userId: userId ?? this.userId,
key: key ?? this.key, preferences: preferences ?? this.preferences,
value: value ?? this.value,
); );
UserMetadataEntityData copyWithCompanion( UserMetadataEntityData copyWithCompanion(
i1.UserMetadataEntityCompanion data) { i1.UserMetadataEntityCompanion data) {
return UserMetadataEntityData( return UserMetadataEntityData(
userId: data.userId.present ? data.userId.value : this.userId, userId: data.userId.present ? data.userId.value : this.userId,
key: data.key.present ? data.key.value : this.key, preferences:
value: data.value.present ? data.value.value : this.value, data.preferences.present ? data.preferences.value : this.preferences,
); );
} }
@@ -434,60 +394,49 @@ class UserMetadataEntityData extends i0.DataClass
String toString() { String toString() {
return (StringBuffer('UserMetadataEntityData(') return (StringBuffer('UserMetadataEntityData(')
..write('userId: $userId, ') ..write('userId: $userId, ')
..write('key: $key, ') ..write('preferences: $preferences')
..write('value: $value')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(userId, key, value); int get hashCode => Object.hash(userId, preferences);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is i1.UserMetadataEntityData && (other is i1.UserMetadataEntityData &&
other.userId == this.userId && other.userId == this.userId &&
other.key == this.key && other.preferences == this.preferences);
other.value == this.value);
} }
class UserMetadataEntityCompanion class UserMetadataEntityCompanion
extends i0.UpdateCompanion<i1.UserMetadataEntityData> { extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
final i0.Value<String> userId; final i0.Value<String> userId;
final i0.Value<i2.UserMetadataKey> key; final i0.Value<i2.UserPreferences> preferences;
final i0.Value<Map<String, Object?>> value;
const UserMetadataEntityCompanion({ const UserMetadataEntityCompanion({
this.userId = const i0.Value.absent(), this.userId = const i0.Value.absent(),
this.key = const i0.Value.absent(), this.preferences = const i0.Value.absent(),
this.value = const i0.Value.absent(),
}); });
UserMetadataEntityCompanion.insert({ UserMetadataEntityCompanion.insert({
required String userId, required String userId,
required i2.UserMetadataKey key, required i2.UserPreferences preferences,
required Map<String, Object?> value,
}) : userId = i0.Value(userId), }) : userId = i0.Value(userId),
key = i0.Value(key), preferences = i0.Value(preferences);
value = i0.Value(value);
static i0.Insertable<i1.UserMetadataEntityData> custom({ static i0.Insertable<i1.UserMetadataEntityData> custom({
i0.Expression<String>? userId, i0.Expression<String>? userId,
i0.Expression<int>? key, i0.Expression<String>? preferences,
i0.Expression<i3.Uint8List>? value,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (userId != null) 'user_id': userId, if (userId != null) 'user_id': userId,
if (key != null) 'key': key, if (preferences != null) 'preferences': preferences,
if (value != null) 'value': value,
}); });
} }
i1.UserMetadataEntityCompanion copyWith( i1.UserMetadataEntityCompanion copyWith(
{i0.Value<String>? userId, {i0.Value<String>? userId, i0.Value<i2.UserPreferences>? preferences}) {
i0.Value<i2.UserMetadataKey>? key,
i0.Value<Map<String, Object?>>? value}) {
return i1.UserMetadataEntityCompanion( return i1.UserMetadataEntityCompanion(
userId: userId ?? this.userId, userId: userId ?? this.userId,
key: key ?? this.key, preferences: preferences ?? this.preferences,
value: value ?? this.value,
); );
} }
@@ -497,13 +446,10 @@ class UserMetadataEntityCompanion
if (userId.present) { if (userId.present) {
map['user_id'] = i0.Variable<String>(userId.value); map['user_id'] = i0.Variable<String>(userId.value);
} }
if (key.present) { if (preferences.present) {
map['key'] = i0.Variable<int>( map['preferences'] = i0.Variable<String>(i1
i1.$UserMetadataEntityTable.$converterkey.toSql(key.value)); .$UserMetadataEntityTable.$converterpreferences
} .toSql(preferences.value));
if (value.present) {
map['value'] = i0.Variable<i3.Uint8List>(
i1.$UserMetadataEntityTable.$convertervalue.toSql(value.value));
} }
return map; return map;
} }
@@ -512,8 +458,7 @@ class UserMetadataEntityCompanion
String toString() { String toString() {
return (StringBuffer('UserMetadataEntityCompanion(') return (StringBuffer('UserMetadataEntityCompanion(')
..write('userId: $userId, ') ..write('userId: $userId, ')
..write('key: $key, ') ..write('preferences: $preferences')
..write('value: $value')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@@ -55,6 +55,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
], ],
include: { include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift', 'package:immich_mobile/infrastructure/entities/merged_asset.drift',
'package:immich_mobile/infrastructure/entities/asset_triggers.drift',
}, },
) )
class Drift extends $Drift implements IDatabaseRepository { class Drift extends $Drift implements IDatabaseRepository {

View File

@@ -7,31 +7,33 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
as i2; as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i3; as i3;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_triggers.drift.dart'
as i4; as i4;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i5; as i5;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i6; as i6;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i7; as i7;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i8; as i8;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i9; as i9;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i10; as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i11; as i11;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i12; as i12;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i13; as i13;
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i14; as i14;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i15; as i15;
import 'package:drift/internal/modular.dart' as i16; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i16;
import 'package:drift/internal/modular.dart' as i17;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@@ -41,28 +43,28 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.$RemoteAssetEntityTable(this); i2.$RemoteAssetEntityTable(this);
late final i3.$LocalAssetEntityTable localAssetEntity = late final i3.$LocalAssetEntityTable localAssetEntity =
i3.$LocalAssetEntityTable(this); i3.$LocalAssetEntityTable(this);
late final i4.$UserMetadataEntityTable userMetadataEntity = late final i5.$UserMetadataEntityTable userMetadataEntity =
i4.$UserMetadataEntityTable(this); i5.$UserMetadataEntityTable(this);
late final i5.$PartnerEntityTable partnerEntity = late final i6.$PartnerEntityTable partnerEntity =
i5.$PartnerEntityTable(this); i6.$PartnerEntityTable(this);
late final i6.$LocalAlbumEntityTable localAlbumEntity = late final i7.$LocalAlbumEntityTable localAlbumEntity =
i6.$LocalAlbumEntityTable(this); i7.$LocalAlbumEntityTable(this);
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
i7.$LocalAlbumAssetEntityTable(this); i8.$LocalAlbumAssetEntityTable(this);
late final i8.$RemoteExifEntityTable remoteExifEntity = late final i9.$RemoteExifEntityTable remoteExifEntity =
i8.$RemoteExifEntityTable(this); i9.$RemoteExifEntityTable(this);
late final i9.$RemoteAlbumEntityTable remoteAlbumEntity = late final i10.$RemoteAlbumEntityTable remoteAlbumEntity =
i9.$RemoteAlbumEntityTable(this); i10.$RemoteAlbumEntityTable(this);
late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity =
i10.$RemoteAlbumAssetEntityTable(this); i11.$RemoteAlbumAssetEntityTable(this);
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
i11.$RemoteAlbumUserEntityTable(this); i12.$RemoteAlbumUserEntityTable(this);
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this); late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this);
late final i13.$MemoryAssetEntityTable memoryAssetEntity = late final i14.$MemoryAssetEntityTable memoryAssetEntity =
i13.$MemoryAssetEntityTable(this); i14.$MemoryAssetEntityTable(this);
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this); late final i15.$StackEntityTable stackEntity = i15.$StackEntityTable(this);
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this) i16.MergedAssetDrift get mergedAssetDrift => i17.ReadDatabaseContainer(this)
.accessor<i15.MergedAssetDrift>(i15.MergedAssetDrift.new); .accessor<i16.MergedAssetDrift>(i16.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -71,6 +73,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
userEntity, userEntity,
remoteAssetEntity, remoteAssetEntity,
localAssetEntity, localAssetEntity,
i4.trLocalAssetUpdateChecksumSetIds,
i4.trLocalAssetUpdateOldChecksumSetRemoteAssetLocalId,
i4.trLocalAssetDeleteUpdateRemoteAssetLocalId,
i4.trRemoteAssetInsertSetLocalId,
i3.idxLocalAssetChecksum, i3.idxLocalAssetChecksum,
i2.uQRemoteAssetOwnerChecksum, i2.uQRemoteAssetOwnerChecksum,
i2.idxRemoteAssetChecksum, i2.idxRemoteAssetChecksum,
@@ -97,6 +103,43 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
], ],
), ),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('local_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.update),
result: [
i0.TableUpdate('local_asset_entity', kind: i0.UpdateKind.update),
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.update),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.insert),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.update),
i0.TableUpdate('local_asset_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation( i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity', on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete), limitUpdateKind: i0.UpdateKind.delete),
@@ -232,27 +275,27 @@ class $DriftManager {
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
i3.$$LocalAssetEntityTableTableManager get localAssetEntity => i3.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); i3.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i4.$$UserMetadataEntityTableTableManager get userMetadataEntity => i5.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i4.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i5.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i5.$$PartnerEntityTableTableManager get partnerEntity => i6.$$PartnerEntityTableTableManager get partnerEntity =>
i5.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); i6.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity => i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7 i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i10.$$RemoteAlbumAssetEntityTableTableManager( i11.$$RemoteAlbumAssetEntityTableTableManager(
_db, _db.remoteAlbumAssetEntity); _db, _db.remoteAlbumAssetEntity);
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11 i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i12.$$MemoryEntityTableTableManager get memoryEntity => i13.$$MemoryEntityTableTableManager get memoryEntity =>
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i14.$$StackEntityTableTableManager get stackEntity => i15.$$StackEntityTableTableManager get stackEntity =>
i14.$$StackEntityTableTableManager(_db, _db.stackEntity); i15.$$StackEntityTableTableManager(_db, _db.stackEntity);
} }

View File

@@ -281,8 +281,8 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
height: Value(asset.height), height: Value(asset.height),
durationInSeconds: Value(asset.durationInSeconds), durationInSeconds: Value(asset.durationInSeconds),
id: asset.id, id: asset.id,
orientation: Value(asset.orientation),
checksum: const Value(null), checksum: const Value(null),
remoteId: const Value(null),
); );
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity, _db.localAssetEntity,
@@ -326,16 +326,12 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return transaction(() async { return transaction(() async {
if (assetsToUnLink.isNotEmpty) { if (assetsToUnLink.isNotEmpty) {
await _db.batch((batch) { await _db.batch(
for (final assetToUnLink in assetsToUnLink) { (batch) => batch.deleteWhere(
batch.deleteWhere( _db.localAlbumAssetEntity,
_db.localAlbumAssetEntity, (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId),
(row) => ),
row.assetId.equals(assetToUnLink) & );
row.albumId.equals(albumId),
);
}
});
} }
await _deleteAssets(assetsToDelete); await _deleteAssets(assetsToDelete);
@@ -363,29 +359,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
} }
return _db.batch((batch) { return _db.batch((batch) {
for (final id in ids) { batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
batch.deleteWhere(_db.localAssetEntity, (row) => row.id.equals(id));
}
}); });
} }
Future<LocalAsset?> getThumbnail(String albumId) async {
final query = _db.localAlbumAssetEntity.select().join([
innerJoin(
_db.localAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
),
])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)])
..limit(1);
final results = await query
.map((row) => row.readTable(_db.localAssetEntity).toDto())
.get();
return results.isNotEmpty ? results.first : null;
}
} }
extension on LocalAlbumEntityData { extension on LocalAlbumEntityData {

View File

@@ -9,23 +9,10 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
const DriftLocalAssetRepository(this._db) : super(_db); const DriftLocalAssetRepository(this._db) : super(_db);
Stream<LocalAsset?> watchAsset(String id) { Stream<LocalAsset?> watchAsset(String id) {
final query = _db.localAssetEntity final query = _db.localAssetEntity.select()
.select() ..where((row) => row.id.equals(id));
.addColumns([_db.remoteAssetEntity.id]).join([
leftOuterJoin(
_db.remoteAssetEntity,
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
useColumns: false,
),
])
..where(_db.localAssetEntity.id.equals(id));
return query.map((row) { return query.map((row) => row.toDto()).watchSingleOrNull();
final asset = row.readTable(_db.localAssetEntity).toDto();
return asset.copyWith(
remoteId: row.read(_db.remoteAssetEntity.id),
);
}).watchSingleOrNull();
} }
Future<void> updateHashes(Iterable<LocalAsset> hashes) { Future<void> updateHashes(Iterable<LocalAsset> hashes) {
@@ -43,12 +30,4 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
} }
}); });
} }
Future<void> delete(List<String> ids) {
return _db.batch((batch) {
for (final id in ids) {
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.equals(id));
}
});
}
} }

View File

@@ -1,24 +1,15 @@
import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
enum SortRemoteAlbumsBy { id, updatedAt } enum SortRemoteAlbumsBy { id }
class DriftRemoteAlbumRepository extends DriftDatabaseRepository { class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
const DriftRemoteAlbumRepository(this._db) : super(_db); const DriftRemoteAlbumRepository(this._db) : super(_db);
Future<List<RemoteAlbum>> getAll({ Future<List<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
Set<SortRemoteAlbumsBy> sortBy = const {SortRemoteAlbumsBy.updatedAt},
}) {
final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); final assetCount = _db.remoteAlbumAssetEntity.assetId.count();
final query = _db.remoteAlbumEntity.select().join([ final query = _db.remoteAlbumEntity.select().join([
@@ -27,21 +18,13 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id), _db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false, useColumns: false,
), ),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin( leftOuterJoin(
_db.userEntity, _db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId), _db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
), ),
]); ]);
query query
..where(_db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount]) ..addColumns([assetCount])
..addColumns([_db.userEntity.name])
..groupBy([_db.remoteAlbumEntity.id]); ..groupBy([_db.remoteAlbumEntity.id]);
if (sortBy.isNotEmpty) { if (sortBy.isNotEmpty) {
@@ -50,8 +33,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
orderings.add( orderings.add(
switch (sort) { switch (sort) {
SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id), SortRemoteAlbumsBy.id => OrderingTerm.asc(_db.remoteAlbumEntity.id),
SortRemoteAlbumsBy.updatedAt =>
OrderingTerm.desc(_db.remoteAlbumEntity.updatedAt),
}, },
); );
} }
@@ -62,223 +43,16 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.map( .map(
(row) => row.readTable(_db.remoteAlbumEntity).toDto( (row) => row.readTable(_db.remoteAlbumEntity).toDto(
assetCount: row.read(assetCount) ?? 0, assetCount: row.read(assetCount) ?? 0,
ownerName: row.read(_db.userEntity.name)!, ownerName: row.readTable(_db.userEntity).name,
), ),
) )
.get(); .get();
} }
Future<void> create(
RemoteAlbum album,
List<String> assetIds,
) async {
await _db.transaction(() async {
final entity = RemoteAlbumEntityCompanion(
id: Value(album.id),
name: Value(album.name),
ownerId: Value(album.ownerId),
createdAt: Value(album.createdAt),
updatedAt: Value(album.updatedAt),
description: Value(album.description),
thumbnailAssetId: Value(album.thumbnailAssetId),
isActivityEnabled: Value(album.isActivityEnabled),
order: Value(album.order),
);
await _db.remoteAlbumEntity.insertOne(entity);
if (assetIds.isNotEmpty) {
final albumAssets = assetIds.map(
(assetId) => RemoteAlbumAssetEntityCompanion(
albumId: Value(album.id),
assetId: Value(assetId),
),
);
await _db.batch((batch) {
batch.insertAll(
_db.remoteAlbumAssetEntity,
albumAssets,
);
});
}
});
}
Future<void> update(RemoteAlbum album) async {
await _db.remoteAlbumEntity.update().replace(
RemoteAlbumEntityCompanion(
id: Value(album.id),
name: Value(album.name),
ownerId: Value(album.ownerId),
createdAt: Value(album.createdAt),
updatedAt: Value(album.updatedAt),
description: Value(album.description),
thumbnailAssetId: Value(album.thumbnailAssetId),
isActivityEnabled: Value(album.isActivityEnabled),
order: Value(album.order),
),
);
}
Future<void> removeAssets(String albumId, List<String> assetIds) {
return _db.batch((batch) {
for (final assetId in assetIds) {
batch.deleteWhere(
_db.remoteAlbumAssetEntity,
(row) => row.albumId.equals(albumId) & row.assetId.equals(assetId),
);
}
});
}
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
final query = _db.remoteAlbumAssetEntity.selectOnly()
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..addColumns([
_db.remoteAssetEntity.createdAt.min(),
_db.remoteAssetEntity.createdAt.max(),
])
..join([
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id
.equalsExp(_db.remoteAlbumAssetEntity.assetId),
),
]);
return query.map((row) {
final minDate = row.read(_db.remoteAssetEntity.createdAt.min());
final maxDate = row.read(_db.remoteAssetEntity.createdAt.max());
return (minDate ?? DateTime.now(), maxDate ?? DateTime.now());
}).getSingle();
}
Future<List<UserDto>> getSharedUsers(String albumId) async {
final albumUserRows = await (_db.select(_db.remoteAlbumUserEntity)
..where((row) => row.albumId.equals(albumId)))
.get();
if (albumUserRows.isEmpty) {
return [];
}
final userIds = albumUserRows.map((row) => row.userId);
// TODO: remove this isIn() after removing UserDto
return (_db.select(_db.userEntity)..where((row) => row.id.isIn(userIds)))
.map(
(user) => UserDto(
id: user.id,
email: user.email,
name: user.name,
profileImagePath: user.profileImagePath?.isEmpty == true
? null
: user.profileImagePath,
isAdmin: user.isAdmin,
updatedAt: user.updatedAt,
quotaSizeInBytes: user.quotaSizeInBytes ?? 0,
quotaUsageInBytes: user.quotaUsageInBytes,
memoryEnabled: true,
inTimeline: false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
),
)
.get();
}
Future<List<RemoteAsset>> getAssets(String albumId) {
final query = _db.remoteAlbumAssetEntity.select().join([
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
),
])
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId));
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
}
Future<int> addAssets(String albumId, List<String> assetIds) async {
final albumAssets = assetIds.map(
(assetId) => RemoteAlbumAssetEntityCompanion(
albumId: Value(albumId),
assetId: Value(assetId),
),
);
await _db.batch((batch) {
batch.insertAll(
_db.remoteAlbumAssetEntity,
albumAssets,
);
});
return assetIds.length;
}
Future<void> addUsers(String albumId, List<String> userIds) {
final albumUsers = userIds.map(
(assetId) => RemoteAlbumUserEntityCompanion(
albumId: Value(albumId),
userId: Value(assetId),
role: const Value(AlbumUserRole.editor),
),
);
return _db.batch((batch) {
batch.insertAll(
_db.remoteAlbumUserEntity,
albumUsers,
);
});
}
Future<void> deleteAlbum(String albumId) async {
return _db.transaction(() async {
await _db.remoteAlbumEntity.deleteWhere(
(table) => table.id.equals(albumId),
);
});
}
Stream<RemoteAlbum?> watchAlbum(String albumId) {
final query = _db.remoteAlbumEntity.select().join([
leftOuterJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
),
])
..where(_db.remoteAlbumEntity.id.equals(albumId))
..addColumns([_db.userEntity.name])
..groupBy([_db.remoteAlbumEntity.id]);
return query.map((row) {
final album = row.readTable(_db.remoteAlbumEntity).toDto(
ownerName: row.read(_db.userEntity.name)!,
);
return album;
}).watchSingleOrNull();
}
} }
extension on RemoteAlbumEntityData { extension on RemoteAlbumEntityData {
RemoteAlbum toDto({int assetCount = 0, required String ownerName}) { Album toDto({int assetCount = 0, required String ownerName}) {
return RemoteAlbum( return Album(
id: id, id: id,
name: name, name: name,
ownerId: ownerId, ownerId: ownerId,

View File

@@ -30,23 +30,10 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
} }
Stream<RemoteAsset?> watchAsset(String id) { Stream<RemoteAsset?> watchAsset(String id) {
final query = _db.remoteAssetEntity final query = _db.remoteAssetEntity.select()
.select() ..where((row) => row.id.equals(id));
.addColumns([_db.localAssetEntity.id]).join([
leftOuterJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])
..where(_db.remoteAssetEntity.id.equals(id));
return query.map((row) { return query.map((row) => row.toDto()).watchSingleOrNull();
final asset = row.readTable(_db.remoteAssetEntity).toDto();
return asset.copyWith(
localId: row.read(_db.localAssetEntity.id),
);
}).watchSingleOrNull();
} }
Future<ExifInfo?> getExif(String id) { Future<ExifInfo?> getExif(String id) {
@@ -56,42 +43,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
.getSingleOrNull(); .getSingleOrNull();
} }
Future<List<(String, String)>> getPlaces() {
final asset = Subquery(
_db.remoteAssetEntity.select()
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]),
"asset",
);
final query = asset.selectOnly().join([
innerJoin(
_db.remoteExifEntity,
_db.remoteExifEntity.assetId
.equalsExp(asset.ref(_db.remoteAssetEntity.id)),
useColumns: false,
),
])
..addColumns([
_db.remoteExifEntity.city,
_db.remoteExifEntity.assetId,
])
..where(
_db.remoteExifEntity.city.isNotNull() &
asset.ref(_db.remoteAssetEntity.deletedAt).isNull() &
asset
.ref(_db.remoteAssetEntity.visibility)
.equals(AssetVisibility.timeline.index),
)
..groupBy([_db.remoteExifEntity.city])
..orderBy([OrderingTerm.asc(_db.remoteExifEntity.city)]);
return query.map((row) {
final assetId = row.read(_db.remoteExifEntity.assetId);
final city = row.read(_db.remoteExifEntity.city);
return (city!, assetId!);
}).get();
}
Future<void> updateFavorite(List<String> ids, bool isFavorite) { Future<void> updateFavorite(List<String> ids, bool isFavorite) {
return _db.batch((batch) async { return _db.batch((batch) async {
for (final id in ids) { for (final id in ids) {
@@ -129,14 +80,7 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
} }
Future<void> delete(List<String> ids) { Future<void> delete(List<String> ids) {
return _db.batch((batch) { return _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(ids));
for (final id in ids) {
batch.deleteWhere(
_db.remoteAssetEntity,
(row) => row.id.equals(id),
);
}
});
} }
Future<void> updateLocation(List<String> ids, LatLng location) { Future<void> updateLocation(List<String> ids, LatLng location) {

View File

@@ -1,22 +1,29 @@
import 'dart:io'; import 'dart:io';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
class StorageRepository { class StorageRepository {
const StorageRepository(); const StorageRepository();
Future<File?> getFileForAsset(String assetId) async { Future<File?> getFileForAsset(LocalAsset asset) async {
final log = Logger('StorageRepository'); final log = Logger('StorageRepository');
File? file; File? file;
try { try {
final entity = await AssetEntity.fromId(assetId); final entity = await AssetEntity.fromId(asset.id);
file = await entity?.originFile; file = await entity?.originFile;
if (file == null) { if (file == null) {
log.warning("Cannot get file for asset $assetId"); log.warning(
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
);
} }
} catch (error, stackTrace) { } catch (error, stackTrace) {
log.warning("Error getting file for asset $assetId", error, stackTrace); log.warning(
"Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
error,
stackTrace,
);
} }
return file; return file;
} }

View File

@@ -56,7 +56,6 @@ class SyncApiRepository {
SyncRequestType.memoryToAssetsV1, SyncRequestType.memoryToAssetsV1,
SyncRequestType.stacksV1, SyncRequestType.stacksV1,
SyncRequestType.partnerStacksV1, SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1,
], ],
).toJson(), ).toJson(),
); );
@@ -171,8 +170,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerStackV1: SyncStackV1.fromJson, SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson, SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
SyncEntityType.userMetadataV1: SyncUserMetadataV1.fromJson,
SyncEntityType.userMetadataDeleteV1: SyncUserMetadataDeleteV1.fromJson,
}; };
class _SyncAckV1 { class _SyncAckV1 {

View File

@@ -4,18 +4,16 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole; import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole;
@@ -29,14 +27,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async { Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try { try {
await _db.batch((batch) { await _db.userEntity
for (final user in data) { .deleteWhere((row) => row.id.isIn(data.map((e) => e.userId)));
batch.deleteWhere(
_db.userEntity,
(row) => row.id.equals(user.userId),
);
}
});
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Error: SyncUserDeleteV1', error, stack); _logger.severe('Error: SyncUserDeleteV1', error, stack);
rethrow; rethrow;
@@ -112,14 +104,9 @@ class SyncStreamRepository extends DriftDatabaseRepository {
String debugLabel = 'user', String debugLabel = 'user',
}) async { }) async {
try { try {
await _db.batch((batch) { await _db.remoteAssetEntity.deleteWhere(
for (final asset in data) { (row) => row.id.isIn(data.map((e) => e.assetId)),
batch.deleteWhere( );
_db.remoteAssetEntity,
(row) => row.id.equals(asset.assetId),
);
}
});
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack); _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
rethrow; rethrow;
@@ -147,7 +134,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
thumbHash: Value(asset.thumbhash), thumbHash: Value(asset.thumbhash),
deletedAt: Value(asset.deletedAt), deletedAt: Value(asset.deletedAt),
visibility: Value(asset.visibility.toAssetVisibility()), visibility: Value(asset.visibility.toAssetVisibility()),
livePhotoVideoId: Value(asset.livePhotoVideoId),
); );
batch.insert( batch.insert(
@@ -213,14 +199,9 @@ class SyncStreamRepository extends DriftDatabaseRepository {
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async { Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
try { try {
await _db.batch((batch) { await _db.remoteAlbumEntity.deleteWhere(
for (final album in data) { (row) => row.id.isIn(data.map((e) => e.albumId)),
batch.deleteWhere( );
_db.remoteAlbumEntity,
(row) => row.id.equals(album.albumId),
);
}
});
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Error: deleteAlbumsV1', error, stack); _logger.severe('Error: deleteAlbumsV1', error, stack);
rethrow; rethrow;
@@ -387,14 +368,9 @@ class SyncStreamRepository extends DriftDatabaseRepository {
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async { Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
try { try {
await _db.batch((batch) { await _db.memoryEntity.deleteWhere(
for (final memory in data) { (row) => row.id.isIn(data.map((e) => e.memoryId)),
batch.deleteWhere( );
_db.memoryEntity,
(row) => row.id.equals(memory.memoryId),
);
}
});
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Error: deleteMemoriesV1', error, stack); _logger.severe('Error: deleteMemoriesV1', error, stack);
rethrow; rethrow;
@@ -476,66 +452,14 @@ class SyncStreamRepository extends DriftDatabaseRepository {
String debugLabel = 'user', String debugLabel = 'user',
}) async { }) async {
try { try {
await _db.batch((batch) { await _db.stackEntity.deleteWhere(
for (final stack in data) { (row) => row.id.isIn(data.map((e) => e.stackId)),
batch.deleteWhere( );
_db.stackEntity,
(row) => row.id.equals(stack.stackId),
);
}
});
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack); _logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
rethrow; rethrow;
} }
} }
Future<void> updateUserMetadatasV1(
Iterable<SyncUserMetadataV1> data,
) async {
try {
await _db.batch((batch) {
for (final userMetadata in data) {
final companion = UserMetadataEntityCompanion(
value: Value(userMetadata.value as Map<String, Object?>),
);
batch.insert(
_db.userMetadataEntity,
companion.copyWith(
userId: Value(userMetadata.userId),
key: Value(userMetadata.key.toUserMetadataKey()),
),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: deleteUserMetadatasV1', error, stack);
rethrow;
}
}
Future<void> deleteUserMetadatasV1(
Iterable<SyncUserMetadataDeleteV1> data,
) async {
try {
await _db.batch((batch) {
for (final userMetadata in data) {
batch.delete(
_db.userMetadataEntity,
UserMetadataEntityCompanion(
userId: Value(userMetadata.userId),
key: Value(userMetadata.key.toUserMetadataKey()),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: deleteUserMetadatasV1', error, stack);
rethrow;
}
}
} }
extension on AssetTypeEnum { extension on AssetTypeEnum {
@@ -581,15 +505,6 @@ extension on api.AssetVisibility {
}; };
} }
extension on String {
UserMetadataKey toUserMetadataKey() => switch (this) {
"onboarding" => UserMetadataKey.onboarding,
"preferences" => UserMetadataKey.preferences,
"license" => UserMetadataKey.license,
_ => throw Exception('Unknown UserMetadataKey value: $this'),
};
}
extension on String { extension on String {
Duration? toDuration() { Duration? toDuration() {
try { try {

View File

@@ -3,13 +3,10 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:stream_transform/stream_transform.dart'; import 'package:stream_transform/stream_transform.dart';
@@ -33,19 +30,19 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.map((users) => users..add(userId)); .map((users) => users..add(userId));
} }
TimelineQuery main(List<String> userIds, GroupAssetsBy groupBy) => ( List<Bucket> _generateBuckets(int count) {
bucketSource: () => _watchMainBucket( final numBuckets = (count / kTimelineNoneSegmentSize).floor();
userIds, final buckets = List.generate(
groupBy: groupBy, numBuckets,
), (_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
assetSource: (offset, count) => _getMainBucketAssets( );
userIds, if (count % kTimelineNoneSegmentSize != 0) {
offset: offset, buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
count: count, }
), return buckets;
); }
Stream<List<Bucket>> _watchMainBucket( Stream<List<Bucket>> watchMainBucket(
List<String> userIds, { List<String> userIds, {
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
@@ -65,7 +62,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.throttle(const Duration(seconds: 3), trailing: true); .throttle(const Duration(seconds: 3), trailing: true);
} }
Future<List<BaseAsset>> _getMainBucketAssets( Future<List<BaseAsset>> getMainBucketAssets(
List<String> userIds, { List<String> userIds, {
required int offset, required int offset,
required int count, required int count,
@@ -73,54 +70,41 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.mergedAssetDrift return _db.mergedAssetDrift
.mergedAsset(userIds, limit: Limit(count, offset)) .mergedAsset(userIds, limit: Limit(count, offset))
.map( .map(
(row) => row.remoteId != null && row.ownerId != null (row) {
? RemoteAsset( return row.remoteId != null && row.ownerId != null
id: row.remoteId!, ? RemoteAsset(
localId: row.localId, id: row.remoteId!,
name: row.name, localId: row.localId,
ownerId: row.ownerId!, name: row.name,
checksum: row.checksum, ownerId: row.ownerId!,
type: row.type, checksum: row.checksum,
createdAt: row.createdAt, type: row.type,
updatedAt: row.updatedAt, createdAt: row.createdAt,
thumbHash: row.thumbHash, updatedAt: row.updatedAt,
width: row.width, thumbHash: row.thumbHash,
height: row.height, width: row.width,
isFavorite: row.isFavorite, height: row.height,
durationInSeconds: row.durationInSeconds, isFavorite: row.isFavorite,
livePhotoVideoId: row.livePhotoVideoId, durationInSeconds: row.durationInSeconds,
) )
: LocalAsset( : LocalAsset(
id: row.localId!, id: row.localId!,
remoteId: row.remoteId, remoteId: row.remoteId,
name: row.name, name: row.name,
checksum: row.checksum, checksum: row.checksum,
type: row.type, type: row.type,
createdAt: row.createdAt, createdAt: row.createdAt,
updatedAt: row.updatedAt, updatedAt: row.updatedAt,
width: row.width, width: row.width,
height: row.height, height: row.height,
isFavorite: row.isFavorite, isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds, durationInSeconds: row.durationInSeconds,
orientation: row.orientation, );
), },
) ).get();
.get();
} }
TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => ( Stream<List<Bucket>> watchLocalAlbumBucket(
bucketSource: () => _watchLocalAlbumBucket(
albumId,
groupBy: groupBy,
),
assetSource: (offset, count) => _getLocalAlbumBucketAssets(
albumId,
offset: offset,
count: count,
),
);
Stream<List<Bucket>> _watchLocalAlbumBucket(
String albumId, { String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
@@ -134,14 +118,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final assetCountExp = _db.localAssetEntity.id.count(); final assetCountExp = _db.localAssetEntity.id.count();
final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy); final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.localAssetEntity.selectOnly().join([ final query = _db.localAssetEntity.selectOnly()
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
),
])
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..join([
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
),
])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]); ..orderBy([OrderingTerm.desc(dateExp)]);
@@ -153,7 +137,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch(); }).watch();
} }
Future<List<BaseAsset>> _getLocalAlbumBucketAssets( Future<List<BaseAsset>> getLocalAlbumBucketAssets(
String albumId, { String albumId, {
required int offset, required int offset,
required int count, required int count,
@@ -163,32 +147,18 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
innerJoin( innerJoin(
_db.localAlbumAssetEntity, _db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
), ),
], ],
) )
..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query return query
.map((row) => row.readTable(_db.localAssetEntity).toDto()) .map((row) => row.readTable(_db.localAssetEntity).toDto())
.get(); .get();
} }
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( Stream<List<Bucket>> watchRemoteAlbumBucket(
bucketSource: () => _watchRemoteAlbumBucket(
albumId,
groupBy: groupBy,
),
assetSource: (offset, count) => _getRemoteAlbumBucketAssets(
albumId,
offset: offset,
count: count,
),
);
Stream<List<Bucket>> _watchRemoteAlbumBucket(
String albumId, { String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
@@ -196,175 +166,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAlbumAssetEntity return _db.remoteAlbumAssetEntity
.count(where: (row) => row.albumId.equals(albumId)) .count(where: (row) => row.albumId.equals(albumId))
.map(_generateBuckets) .map(_generateBuckets)
.watch() .watchSingle();
.map((results) => results.isNotEmpty ? results.first : <Bucket>[])
.handleError((error) {
return [];
});
}
return (_db.remoteAlbumEntity.select()
..where((row) => row.id.equals(albumId)))
.watch()
.switchMap((albums) {
if (albums.isEmpty) {
return Stream.value(<Bucket>[]);
}
final album = albums.first;
final isAscending = album.order == AlbumAssetOrder.asc;
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..join([
innerJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
)
..groupBy([dateExp]);
if (isAscending) {
query.orderBy([OrderingTerm.asc(dateExp)]);
} else {
query.orderBy([OrderingTerm.desc(dateExp)]);
}
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}).handleError((error) {
// If there's an error (e.g., album was deleted), return empty buckets
return <Bucket>[];
});
}
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(
String albumId, {
required int offset,
required int count,
}) async {
final albumData = await (_db.remoteAlbumEntity.select()
..where((row) => row.id.equals(albumId)))
.getSingleOrNull();
// If album doesn't exist (was deleted), return empty list
if (albumData == null) {
return <BaseAsset>[];
}
final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().join(
[
innerJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
],
)..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
);
if (isAscending) {
query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
} else {
query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]);
}
query.limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
}
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(ownerId),
groupBy: groupBy,
);
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
row.isFavorite.equals(true) &
row.ownerId.equals(userId),
groupBy: groupBy,
);
TimelineQuery trash(String userId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder(
filter: (row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
groupBy: groupBy,
);
TimelineQuery archived(String userId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.archive),
groupBy: groupBy,
);
TimelineQuery locked(String userId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
groupBy: groupBy,
);
TimelineQuery video(String userId, GroupAssetsBy groupBy) =>
_remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
groupBy: groupBy,
);
TimelineQuery place(String place, GroupAssetsBy groupBy) => (
bucketSource: () => _watchPlaceBucket(
place,
groupBy: groupBy,
),
assetSource: (offset, count) => _getPlaceBucketAssets(
place,
offset: offset,
count: count,
),
);
Stream<List<Bucket>> _watchPlaceBucket(
String place, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
// TODO: implement GroupAssetBy for place
throw UnsupportedError(
"GroupAssetsBy.none is not supported for watchPlaceBucket",
);
} }
final assetCountExp = _db.remoteAssetEntity.id.count(); final assetCountExp = _db.remoteAssetEntity.id.count();
@@ -374,14 +176,268 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..join([ ..join([
innerJoin( innerJoin(
_db.remoteExifEntity, _db.remoteAlbumAssetEntity,
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id), _db.remoteAlbumAssetEntity.assetId
useColumns: false, .equalsExp(_db.remoteAssetEntity.id),
), ),
]) ])
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getRemoteAlbumBucketAssets(
String albumId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select().join(
[
innerJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id),
),
],
)
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
}
Stream<List<Bucket>> watchFavoriteBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.isFavorite.equals(true) & row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where( ..where(
_db.remoteExifEntity.city.equals(place) & _db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.isFavorite.equals(true),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getFavoriteBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.isFavorite.equals(true) & row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchTrashBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNotNull() & row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNotNull(),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getTrashBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchArchiveBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.visibility.equalsValue(AssetVisibility.archive) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.archive),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getArchiveBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.archive),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchLockedFolderBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.locked),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getLockedFolderBucketAssets(
String userId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchVideoBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility _db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline), .equalsValue(AssetVisibility.timeline),
) )
@@ -395,51 +451,39 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch(); }).watch();
} }
Future<List<BaseAsset>> _getPlaceBucketAssets( Future<List<BaseAsset>> getVideoBucketAssets(
String place, { String userId, {
required int offset, required int offset,
required int count, required int count,
}) { }) {
final query = _db.remoteAssetEntity.select().join( final query = _db.remoteAssetEntity.select()
[
innerJoin(
_db.remoteExifEntity,
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
],
)
..where( ..where(
_db.remoteAssetEntity.deletedAt.isNull() & (row) =>
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility _db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) & .equalsValue(AssetVisibility.timeline) &
_db.remoteExifEntity.city.equals(place), _db.remoteAssetEntity.ownerId.equals(userId),
) )
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto()) return query.map((row) => row.toDto()).get();
.get();
} }
TimelineQuery _remoteQueryBuilder({ Stream<List<Bucket>> watchRemoteBucket(
required Expression<bool> Function($RemoteAssetEntityTable row) filter, List<String> userIds, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
return (
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy),
assetSource: (offset, count) =>
_getRemoteAssets(filter: filter, offset: offset, count: count),
);
}
Stream<List<Bucket>> _watchRemoteBucket({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
if (groupBy == GroupAssetsBy.none) { if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.count(where: filter); return _db.remoteAssetEntity
return query.map(_generateBuckets).watchSingle(); .count(
where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.isIn(userIds),
)
.map(_generateBuckets)
.watchSingle();
} }
final assetCountExp = _db.remoteAssetEntity.id.count(); final assetCountExp = _db.remoteAssetEntity.id.count();
@@ -447,7 +491,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..where(filter(_db.remoteAssetEntity)) ..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.isIn(userIds),
)
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]); ..orderBy([OrderingTerm.desc(dateExp)]);
@@ -458,31 +507,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch(); }).watch();
} }
Future<List<BaseAsset>> _getRemoteAssets({ Future<List<BaseAsset>> getRemoteBucketAssets(
required Expression<bool> Function($RemoteAssetEntityTable row) filter, List<String> userIds, {
required int offset, required int offset,
required int count, required int count,
}) { }) {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where(filter) ..where(
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.isIn(userIds),
)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
} }
} }
List<Bucket> _generateBuckets(int count) {
final buckets = List.generate(
(count / kTimelineNoneSegmentSize).floor(),
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
);
if (count % kTimelineNoneSegmentSize != 0) {
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
}
return buckets;
}
extension on Expression<DateTime> { extension on Expression<DateTime> {
Expression<String> dateFmt(GroupAssetsBy groupBy) { Expression<String> dateFmt(GroupAssetsBy groupBy) {
// DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting // DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting

View File

@@ -1,38 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftUserMetadataRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftUserMetadataRepository(this._db) : super(_db);
Future<List<UserMetadata>> getUserMetadata(String userId) {
final query = _db.userMetadataEntity.select()
..where((e) => e.userId.equals(userId));
return query.map((userMetadata) {
return userMetadata.toDto();
}).get();
}
}
extension on UserMetadataEntityData {
UserMetadata toDto() => switch (key) {
UserMetadataKey.onboarding => UserMetadata(
userId: userId,
key: key,
onboarding: Onboarding.fromMap(value),
),
UserMetadataKey.preferences => UserMetadata(
userId: userId,
key: key,
preferences: Preferences.fromMap(value),
),
UserMetadataKey.license => UserMetadata(
userId: userId,
key: key,
license: License.fromMap(value),
),
};
}

View File

@@ -23,15 +23,14 @@ import 'package:immich_mobile/providers/theme.provider.dart';
import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/services/deep_link.service.dart';
import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/cache/widgets_binding.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart';
import 'package:immich_mobile/services/deep_link.service.dart';
import 'package:immich_mobile/utils/download.dart'; import 'package:immich_mobile/utils/download.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:immich_mobile/utils/licenses.dart';
import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/migration.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -99,17 +98,6 @@ Future<void> initApp() async {
); );
await FileDownloader().trackTasks(); await FileDownloader().trackTasks();
LicenseRegistry.addLicense(
() async* {
for (final license in nonPubLicenses.entries) {
yield LicenseEntryWithLineBreaks(
[license.key],
license.value,
);
}
},
);
} }
class ImmichApp extends ConsumerStatefulWidget { class ImmichApp extends ConsumerStatefulWidget {
@@ -188,13 +176,10 @@ class ImmichAppState extends ConsumerState<ImmichApp>
final deepLinkHandler = ref.read(deepLinkServiceProvider); final deepLinkHandler = ref.read(deepLinkServiceProvider);
final currentRouteName = ref.read(currentRouteNameProvider.notifier).state; final currentRouteName = ref.read(currentRouteNameProvider.notifier).state;
final isColdStart =
currentRouteName == null || currentRouteName == SplashScreenRoute.name;
if (deepLink.uri.scheme == "immich") { if (deepLink.uri.scheme == "immich") {
final proposedRoute = await deepLinkHandler.handleScheme( final proposedRoute = await deepLinkHandler.handleScheme(
deepLink, deepLink,
isColdStart, currentRouteName == SplashScreenRoute.name,
); );
return proposedRoute; return proposedRoute;
@@ -203,7 +188,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
if (deepLink.uri.host == "my.immich.app") { if (deepLink.uri.host == "my.immich.app") {
final proposedRoute = await deepLinkHandler.handleMyImmichApp( final proposedRoute = await deepLinkHandler.handleMyImmichApp(
deepLink, deepLink,
isColdStart, currentRouteName == SplashScreenRoute.name,
); );
return proposedRoute; return proposedRoute;
@@ -265,8 +250,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
), ),
routerConfig: router.config( routerConfig: router.config(
deepLinkBuilder: _deepLinkBuilder, deepLinkBuilder: _deepLinkBuilder,
navigatorObservers: () => navigatorObservers: () => [AppNavigationObserver(ref: ref)],
[AppNavigationObserver(ref: ref), HeroController()],
), ),
), ),
); );

View File

@@ -1,40 +1,52 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_action_filled_button.dart';
// ignore: must_be_immutable
class AlbumControlButton extends ConsumerWidget { class AlbumControlButton extends ConsumerWidget {
final void Function()? onAddPhotosPressed; void Function() onAddPhotosPressed;
final void Function()? onAddUsersPressed; void Function() onAddUsersPressed;
const AlbumControlButton({ AlbumControlButton({
super.key, super.key,
this.onAddPhotosPressed, required this.onAddPhotosPressed,
this.onAddUsersPressed, required this.onAddUsersPressed,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return SizedBox( final userId = ref.watch(authProvider).userId;
height: 36, final isOwner = ref.watch(
child: ListView( currentAlbumProvider.select((album) {
scrollDirection: Axis.horizontal, return album?.ownerId == userId;
children: [ }),
if (onAddPhotosPressed != null) );
return Padding(
padding: const EdgeInsets.only(left: 16.0),
child: SizedBox(
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
AlbumActionFilledButton( AlbumActionFilledButton(
key: const ValueKey('add_photos_button'), key: const ValueKey('add_photos_button'),
iconData: Icons.add_photo_alternate_outlined, iconData: Icons.add_photo_alternate_outlined,
onPressed: onAddPhotosPressed, onPressed: onAddPhotosPressed,
labelText: "add_photos".tr(), labelText: "add_photos".tr(),
), ),
if (onAddUsersPressed != null) if (isOwner)
AlbumActionFilledButton( AlbumActionFilledButton(
key: const ValueKey('add_users_button'), key: const ValueKey('add_users_button'),
iconData: Icons.person_add_alt_rounded, iconData: Icons.person_add_alt_rounded,
onPressed: onAddUsersPressed, onPressed: onAddUsersPressed,
labelText: "album_viewer_page_share_add_users".tr(), labelText: "album_viewer_page_share_add_users".tr(),
), ),
], ],
),
), ),
); );
} }

View File

@@ -41,11 +41,6 @@ class AlbumViewer extends HookConsumerWidget {
final userId = ref.watch(authProvider).userId; final userId = ref.watch(authProvider).userId;
final isMultiselecting = ref.watch(multiselectProvider); final isMultiselecting = ref.watch(multiselectProvider);
final isProcessing = useProcessingOverlay(); final isProcessing = useProcessingOverlay();
final isOwner = ref.watch(
currentAlbumProvider.select((album) {
return album?.ownerId == userId;
}),
);
Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async { Future<bool> onRemoveFromAlbumPressed(Iterable<Asset> assets) async {
final bool isSuccess = final bool isSuccess =
@@ -143,13 +138,10 @@ class AlbumViewer extends HookConsumerWidget {
), ),
const AlbumSharedUserIcons(), const AlbumSharedUserIcons(),
if (album.isRemote) if (album.isRemote)
Padding( AlbumControlButton(
padding: const EdgeInsets.only(left: 16.0), key: const ValueKey("albumControlButton"),
child: AlbumControlButton( onAddPhotosPressed: onAddPhotosPressed,
key: const ValueKey("albumControlButton"), onAddUsersPressed: onAddUsersPressed,
onAddPhotosPressed: onAddPhotosPressed,
onAddUsersPressed: isOwner ? onAddUsersPressed : null,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],

View File

@@ -4,13 +4,13 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart';
@RoutePage() @RoutePage()
class TabControllerPage extends HookConsumerWidget { class TabControllerPage extends HookConsumerWidget {
@@ -158,6 +158,10 @@ class TabControllerPage extends HookConsumerWidget {
), ),
builder: (context, child) { builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context); final tabsRouter = AutoTabsRouter.of(context);
final heroedChild = HeroControllerScope(
controller: HeroController(),
child: child,
);
return PopScope( return PopScope(
canPop: tabsRouter.activeIndex == 0, canPop: tabsRouter.activeIndex == 0,
onPopInvokedWithResult: (didPop, _) => onPopInvokedWithResult: (didPop, _) =>
@@ -169,10 +173,10 @@ class TabControllerPage extends HookConsumerWidget {
children: [ children: [
navigationRail(tabsRouter), navigationRail(tabsRouter),
const VerticalDivider(), const VerticalDivider(),
Expanded(child: child), Expanded(child: heroedChild),
], ],
) )
: child, : heroedChild,
bottomNavigationBar: multiselectEnabled || isScreenLandscape bottomNavigationBar: multiselectEnabled || isScreenLandscape
? null ? null
: bottomNavigationBar(tabsRouter), : bottomNavigationBar(tabsRouter),

View File

@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -128,6 +127,10 @@ class TabShellPage extends ConsumerWidget {
), ),
builder: (context, child) { builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context); final tabsRouter = AutoTabsRouter.of(context);
final heroedChild = HeroControllerScope(
controller: HeroController(),
child: child,
);
return PopScope( return PopScope(
canPop: tabsRouter.activeIndex == 0, canPop: tabsRouter.activeIndex == 0,
onPopInvokedWithResult: (didPop, _) => onPopInvokedWithResult: (didPop, _) =>
@@ -139,10 +142,10 @@ class TabShellPage extends ConsumerWidget {
children: [ children: [
navigationRail(tabsRouter), navigationRail(tabsRouter),
const VerticalDivider(), const VerticalDivider(),
Expanded(child: child), Expanded(child: heroedChild),
], ],
) )
: child, : heroedChild,
bottomNavigationBar: _BottomNavigationBar( bottomNavigationBar: _BottomNavigationBar(
tabsRouter: tabsRouter, tabsRouter: tabsRouter,
destinations: navigationDestinations, destinations: navigationDestinations,
@@ -165,11 +168,6 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
ref.read(searchInputFocusProvider).requestFocus(); ref.read(searchInputFocusProvider).requestFocus();
} }
// Album page
if (index == 2) {
ref.read(remoteAlbumProvider.notifier).getAll();
}
ref.read(hapticFeedbackProvider.notifier).selectionClick(); ref.read(hapticFeedbackProvider.notifier).selectionClick();
router.setActiveIndex(index); router.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index]; ref.read(tabProvider.notifier).state = TabEnum.values[index];

View File

@@ -40,7 +40,6 @@ class PlatformAsset {
this.width, this.width,
this.height, this.height,
required this.durationInSeconds, required this.durationInSeconds,
required this.orientation,
}); });
String id; String id;
@@ -59,8 +58,6 @@ class PlatformAsset {
int durationInSeconds; int durationInSeconds;
int orientation;
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[ return <Object?>[
id, id,
@@ -71,7 +68,6 @@ class PlatformAsset {
width, width,
height, height,
durationInSeconds, durationInSeconds,
orientation,
]; ];
} }
@@ -90,7 +86,6 @@ class PlatformAsset {
width: result[5] as int?, width: result[5] as int?,
height: result[6] as int?, height: result[6] as int?,
durationInSeconds: result[7]! as int, durationInSeconds: result[7]! as int,
orientation: result[8]! as int,
); );
} }

View File

@@ -1,12 +1,9 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage() @RoutePage()
class DriftArchivePage extends StatelessWidget { class DriftArchivePage extends StatelessWidget {
@@ -30,13 +27,7 @@ class DriftArchivePage extends StatelessWidget {
}, },
), ),
], ],
child: Timeline( child: const Timeline(),
appBar: MesmerizingSliverAppBar(
title: 'archive'.t(context: context),
icon: Icons.archive_outlined,
),
bottomSheet: const ArchiveBottomSheet(),
),
); );
} }
} }

View File

@@ -1,12 +1,9 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage() @RoutePage()
class DriftFavoritePage extends StatelessWidget { class DriftFavoritePage extends StatelessWidget {
@@ -30,13 +27,7 @@ class DriftFavoritePage extends StatelessWidget {
}, },
), ),
], ],
child: Timeline( child: const Timeline(),
appBar: MesmerizingSliverAppBar(
title: 'favorites'.t(context: context),
icon: Icons.favorite_outline,
),
bottomSheet: const FavoriteBottomSheet(),
),
); );
} }
} }

View File

@@ -6,8 +6,8 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage() @RoutePage()
class DriftRecentlyTakenPage extends StatelessWidget { class DriftLockedFolderPage extends StatelessWidget {
const DriftRecentlyTakenPage({super.key}); const DriftLockedFolderPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -17,13 +17,11 @@ class DriftRecentlyTakenPage extends StatelessWidget {
(ref) { (ref) {
final user = ref.watch(currentUserProvider); final user = ref.watch(currentUserProvider);
if (user == null) { if (user == null) {
throw Exception( throw Exception('User must be logged in to access locked folder');
'User must be logged in to access recently taken',
);
} }
final timelineService = final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(user.id); ref.watch(timelineFactoryProvider).lockedFolder(user.id);
ref.onDispose(timelineService.dispose); ref.onDispose(timelineService.dispose);
return timelineService; return timelineService;
}, },

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@@ -28,16 +27,7 @@ class DriftTrashPage extends StatelessWidget {
}, },
), ),
], ],
child: Timeline( child: const Timeline(),
appBar: SliverAppBar(
title: Text('trash'.t(context: context)),
floating: true,
snap: true,
pinned: true,
centerTitle: true,
elevation: 0,
),
),
); );
} }
} }

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