mirror of
https://github.com/immich-app/immich.git
synced 2025-12-11 07:11:05 -08:00
Compare commits
78 Commits
feat/inlin
...
chore-hand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36cffbd44a | ||
|
|
531515daf9 | ||
|
|
b256c51b6b | ||
|
|
238dc7c085 | ||
|
|
184c7390a1 | ||
|
|
649221176c | ||
|
|
eae2471ab5 | ||
|
|
bfceed15da | ||
|
|
d9891f759e | ||
|
|
32f23b8d38 | ||
|
|
743b6644e9 | ||
|
|
34620e1e9a | ||
|
|
bcb968e3d1 | ||
|
|
e73abe0762 | ||
|
|
920d7de349 | ||
|
|
351701c4d6 | ||
|
|
68f249bc03 | ||
|
|
eca54871d0 | ||
|
|
b359eea124 | ||
|
|
c18f167e29 | ||
|
|
ba262fbaa8 | ||
|
|
59e7754bdc | ||
|
|
0acbf1199a | ||
|
|
daea57f7d2 | ||
|
|
82c3165247 | ||
|
|
3a854d77ac | ||
|
|
ccd0c35ca1 | ||
|
|
5f10a4cae7 | ||
|
|
9abb95d34a | ||
|
|
805ec3e351 | ||
|
|
a97ba4862f | ||
|
|
c699df002a | ||
|
|
33c29e4305 | ||
|
|
b0098d6d23 | ||
|
|
04aab6ecce | ||
|
|
47c0dc0d7e | ||
|
|
df581cc0d5 | ||
|
|
9e48ae3052 | ||
|
|
1d19d308e2 | ||
|
|
de4217cefc | ||
|
|
617a2f146d | ||
|
|
2b07d7ac63 | ||
|
|
1cc5ca14ca | ||
|
|
a625921e8f | ||
|
|
a17bba3328 | ||
|
|
4b3a4725c6 | ||
|
|
34f0f6c813 | ||
|
|
906d14c172 | ||
|
|
d087f7c870 | ||
|
|
de345a9524 | ||
|
|
badd7ea2a9 | ||
|
|
7d8f56b483 | ||
|
|
70b73145f1 | ||
|
|
d178c52ba6 | ||
|
|
55fe67dd20 | ||
|
|
ed4c7817e7 | ||
|
|
39c95f1280 | ||
|
|
4ddd3764b4 | ||
|
|
68db17028b | ||
|
|
1f50a0075e | ||
|
|
b19884d01e | ||
|
|
feff1899ee | ||
|
|
977d6452f6 | ||
|
|
f778adea92 | ||
|
|
818bdde317 | ||
|
|
fd48a33686 | ||
|
|
a918481c0b | ||
|
|
a201665b7e | ||
|
|
2a222fcfba | ||
|
|
d902e7f87d | ||
|
|
6278fe43c0 | ||
|
|
dfe6d27bbd | ||
|
|
51ab7498e9 | ||
|
|
4db76ddcf0 | ||
|
|
d03eb87058 | ||
|
|
a556de67b0 | ||
|
|
e703685d8d | ||
|
|
172388c455 |
@@ -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-dev
|
run_cmd make setup-web-dev setup-server-dev
|
||||||
)
|
)
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
|||||||
8
.github/workflows/build-mobile.yml
vendored
8
.github/workflows/build-mobile.yml
vendored
@@ -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
|
flutter build apk --release --flavor production
|
||||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
flutter build apk --release --flavor production --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||||
else
|
else
|
||||||
flutter build apk --debug --split-per-abi --target-platform android-arm64
|
flutter build apk --debug --flavor production --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
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -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@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
|
||||||
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@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@129aeda75a450666ce96e8bc8126652e717917a7 # multi-runner-build-workflow-0.1.1
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
|
|||||||
13
.github/workflows/org-checks.yml
vendored
Normal file
13
.github/workflows/org-checks.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
3
.github/workflows/static_analysis.yml
vendored
3
.github/workflows/static_analysis.yml
vendored
@@ -61,8 +61,7 @@ jobs:
|
|||||||
run: dart pub get
|
run: dart pub get
|
||||||
|
|
||||||
- name: Install DCM
|
- name: Install DCM
|
||||||
# TODO: Move to upstream after https://github.com/CQLabs/setup-dcm/pull/235 merges
|
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
|
||||||
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
19
.vscode/launch.json
vendored
@@ -18,6 +18,25 @@
|
|||||||
"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"
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -89,7 +89,7 @@ test-medium-dev:
|
|||||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||||
|
|
||||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||||
@@ -106,4 +106,5 @@ 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-dev: install-server install-sdk build-sdk install-web
|
setup-server-dev: install-server
|
||||||
|
setup-web-dev: install-sdk build-sdk install-web
|
||||||
|
|||||||
162
cli/package-lock.json
generated
162
cli/package-lock.json
generated
@@ -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": "^6.0.0",
|
"vite": "^7.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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
|
||||||
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
|
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
|
||||||
"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.35.1",
|
"@typescript-eslint/scope-manager": "8.36.0",
|
||||||
"@typescript-eslint/type-utils": "8.35.1",
|
"@typescript-eslint/type-utils": "8.36.0",
|
||||||
"@typescript-eslint/utils": "8.35.1",
|
"@typescript-eslint/utils": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1",
|
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||||
"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.35.1",
|
"@typescript-eslint/parser": "^8.36.0",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
|
||||||
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
|
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.35.1",
|
"@typescript-eslint/scope-manager": "8.36.0",
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.35.1",
|
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1",
|
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
||||||
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
|
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.35.1",
|
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
||||||
"@typescript-eslint/types": "^8.35.1",
|
"@typescript-eslint/types": "^8.36.0",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
||||||
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
|
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1"
|
"@typescript-eslint/visitor-keys": "8.36.0"
|
||||||
},
|
},
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
||||||
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
|
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
|
||||||
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
|
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.35.1",
|
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||||
"@typescript-eslint/utils": "8.35.1",
|
"@typescript-eslint/utils": "8.36.0",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||||
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
|
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
||||||
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
|
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.35.1",
|
"@typescript-eslint/project-service": "8.36.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.35.1",
|
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1",
|
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
||||||
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
|
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
||||||
"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.35.1",
|
"@typescript-eslint/scope-manager": "8.36.0",
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.35.1"
|
"@typescript-eslint/typescript-estree": "8.36.0"
|
||||||
},
|
},
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
||||||
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
|
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"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.3",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3486,7 +3486,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.11",
|
||||||
"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.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
|
||||||
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
|
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.35.1",
|
"@typescript-eslint/eslint-plugin": "8.36.0",
|
||||||
"@typescript-eslint/parser": "8.35.1",
|
"@typescript-eslint/parser": "8.36.0",
|
||||||
"@typescript-eslint/utils": "8.35.1"
|
"@typescript-eslint/utils": "8.36.0"
|
||||||
},
|
},
|
||||||
"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": "6.3.5",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
||||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.6",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.34.9",
|
"rollup": "^4.40.0",
|
||||||
"tinyglobby": "^0.2.13"
|
"tinyglobby": "^0.2.14"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
"node": "^20.19.0 || >=22.12.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": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "*",
|
"less": "^4.0.0",
|
||||||
"lightningcss": "^1.21.0",
|
"lightningcss": "^1.21.0",
|
||||||
"sass": "*",
|
"sass": "^1.70.0",
|
||||||
"sass-embedded": "*",
|
"sass-embedded": "^1.70.0",
|
||||||
"stylus": "*",
|
"stylus": ">=0.54.8",
|
||||||
"sugarss": "*",
|
"sugarss": "^5.0.0",
|
||||||
"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.4",
|
"version": "6.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||||
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
|
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -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": "^6.0.0",
|
"vite": "^7.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",
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||||
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:7a34573f0b9c952286b33d537f233cd5b708e12263733aa646e50c33f598f16c
|
image: prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fec42f399876eb6faf9e008570597741c87ff7662a54185593e74b09ce83d177
|
image: docker.io/valkey/valkey:8-bookworm@sha256:facc1d2c3462975c34e10fccb167bfa92b0e0dbd992fc282c29a61c3243afb11
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
BIN
docs/docs/administration/img/admin-nightly-tasks.webp
Normal file
BIN
docs/docs/administration/img/admin-nightly-tasks.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -46,6 +46,12 @@ 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.
|
||||||
|
:::
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ sidebar_position: 3
|
|||||||
|
|
||||||
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
|
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
|
||||||
|
|
||||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/)
|
Get started fast!
|
||||||
|
|
||||||
[](https://codespaces.new/immich-app/immich/)
|
[](https://codespaces.new/immich-app/immich/)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ cd immich
|
|||||||
|
|
||||||
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
|
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
|
||||||
|
|
||||||
:::important Required Configuration
|
:::important Configuration
|
||||||
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
|
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -88,6 +88,10 @@ source ~/.bashrc
|
|||||||
|
|
||||||
### Step 3: Launch the Dev Container
|
### Step 3: Launch the Dev Container
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
Immich development makes extensive use of specialized [base images](https://github.com/immich-app/base-images) for its docker-compose based development. For this reason, you won't be able to use VSCode's **_Clone Repository in a Container Volume_** command.
|
||||||
|
:::
|
||||||
|
|
||||||
#### Using VS Code UI:
|
#### Using VS Code UI:
|
||||||
|
|
||||||
1. Open the cloned repository in VS Code
|
1. Open the cloned repository in VS Code
|
||||||
|
|||||||
@@ -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 > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
You can enable this feature under [`Account Settings > Features > Folders`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
||||||
|
|
||||||
## Enable folder view
|
## Enable folder view
|
||||||
|
|
||||||
|
|||||||
@@ -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 - 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.
|
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.
|
||||||
|
|
||||||
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 on the upper right corner
|
- Click on your avatar in 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,9 +159,7 @@ 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 > Folder View`](https://my.immich.app/user-settings?isOpen=feature+folders)
|
You can enable this feature under [`Account Settings > Features > Folders`](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' />
|
||||||
|
|
||||||
@@ -171,7 +169,7 @@ The UI is currently only available for the web; mobile will come in a subsequent
|
|||||||
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 -> Library.
|
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> External 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' />
|
||||||
|
|||||||
@@ -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 **/usr/src/app/external** as the path and click Add
|
- Enter **/home/user/photos1** 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
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ 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),
|
||||||
@@ -196,14 +197,6 @@ 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,
|
||||||
@@ -239,12 +232,26 @@ 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)',
|
title: 'Google Cast (web and mobile)',
|
||||||
description: 'Cast assets to Google Cast/Chromecast compatible devices',
|
description: 'Cast assets to Google Cast/Chromecast compatible devices',
|
||||||
release: 'v1.133.0',
|
release: 'v1.135.0',
|
||||||
}),
|
}),
|
||||||
withRelease({
|
withRelease({
|
||||||
icon: mdiLockOutline,
|
icon: mdiLockOutline,
|
||||||
|
|||||||
5
docs/static/_redirects
vendored
5
docs/static/_redirects
vendored
@@ -1,4 +1,5 @@
|
|||||||
/docs /docs/overview/introduction 307
|
/docs /docs/overview/welcome 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
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
/docs/guides/api-album-sync /docs/community-projects 307
|
/docs/guides/api-album-sync /docs/community-projects 307
|
||||||
/docs/guides/remove-offline-files /docs/community-projects 307
|
/docs/guides/remove-offline-files /docs/community-projects 307
|
||||||
/milestones /roadmap 307
|
/milestones /roadmap 307
|
||||||
/docs/overview/introduction /docs/overview/welcome 307
|
/docs/overview/introduction /docs/overview/welcome 307
|
||||||
|
|||||||
141
e2e/package-lock.json
generated
141
e2e/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@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",
|
||||||
@@ -82,7 +83,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": "^6.0.0",
|
"vite": "^7.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",
|
||||||
@@ -2100,17 +2101,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
|
||||||
"integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==",
|
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
|
||||||
"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.35.1",
|
"@typescript-eslint/scope-manager": "8.36.0",
|
||||||
"@typescript-eslint/type-utils": "8.35.1",
|
"@typescript-eslint/type-utils": "8.36.0",
|
||||||
"@typescript-eslint/utils": "8.35.1",
|
"@typescript-eslint/utils": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1",
|
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||||
"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",
|
||||||
@@ -2124,7 +2125,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.35.1",
|
"@typescript-eslint/parser": "^8.36.0",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -2140,16 +2141,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
|
||||||
"integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==",
|
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.35.1",
|
"@typescript-eslint/scope-manager": "8.36.0",
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.35.1",
|
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1",
|
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2165,14 +2166,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
||||||
"integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==",
|
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.35.1",
|
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
||||||
"@typescript-eslint/types": "^8.35.1",
|
"@typescript-eslint/types": "^8.36.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2187,14 +2188,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
||||||
"integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==",
|
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1"
|
"@typescript-eslint/visitor-keys": "8.36.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -2205,9 +2206,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
||||||
"integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==",
|
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2222,14 +2223,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
|
||||||
"integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==",
|
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.35.1",
|
"@typescript-eslint/typescript-estree": "8.36.0",
|
||||||
"@typescript-eslint/utils": "8.35.1",
|
"@typescript-eslint/utils": "8.36.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -2246,9 +2247,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||||
"integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==",
|
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2260,16 +2261,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
||||||
"integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==",
|
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.35.1",
|
"@typescript-eslint/project-service": "8.36.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.35.1",
|
"@typescript-eslint/tsconfig-utils": "8.36.0",
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.35.1",
|
"@typescript-eslint/visitor-keys": "8.36.0",
|
||||||
"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",
|
||||||
@@ -2315,16 +2316,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
||||||
"integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==",
|
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
||||||
"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.35.1",
|
"@typescript-eslint/scope-manager": "8.36.0",
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.35.1"
|
"@typescript-eslint/typescript-estree": "8.36.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -2339,13 +2340,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
||||||
"integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==",
|
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.35.1",
|
"@typescript-eslint/types": "8.36.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4811,9 +4812,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/luxon": {
|
"node_modules/luxon": {
|
||||||
"version": "3.6.1",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
|
||||||
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
|
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -6429,9 +6430,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/superagent": {
|
"node_modules/superagent": {
|
||||||
"version": "10.2.1",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz",
|
||||||
"integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
|
"integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6450,14 +6451,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supertest": {
|
"node_modules/supertest": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz",
|
||||||
"integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
|
"integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"methods": "^1.1.2",
|
"methods": "^1.1.2",
|
||||||
"superagent": "^10.2.1"
|
"superagent": "^10.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18.0"
|
"node": ">=14.18.0"
|
||||||
@@ -6777,15 +6778,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.35.1",
|
"version": "8.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
|
||||||
"integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==",
|
"integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.35.1",
|
"@typescript-eslint/eslint-plugin": "8.36.0",
|
||||||
"@typescript-eslint/parser": "8.35.1",
|
"@typescript-eslint/parser": "8.36.0",
|
||||||
"@typescript-eslint/utils": "8.35.1"
|
"@typescript-eslint/utils": "8.36.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@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",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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';
|
||||||
@@ -342,5 +343,36 @@ 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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['api_keys']);
|
await utils.resetDatabase(['api_key']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /api-keys', () => {
|
describe('POST /api-keys', () => {
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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(['tags']);
|
await utils.resetDatabase(['tag']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /tags', () => {
|
describe('POST /tags', () => {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ describe(`immich upload`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['assets', 'albums']);
|
await utils.resetDatabase(['asset', 'album']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`immich upload /path/to/file.jpg`, () => {
|
describe(`immich upload /path/to/file.jpg`, () => {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ 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: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
'asset_stack',
|
'stack',
|
||||||
'libraries',
|
'library',
|
||||||
'shared_links',
|
'shared_link',
|
||||||
'person',
|
'person',
|
||||||
'albums',
|
'album',
|
||||||
'assets',
|
'asset',
|
||||||
'asset_faces',
|
'asset_face',
|
||||||
'activity',
|
'activity',
|
||||||
'api_keys',
|
'api_key',
|
||||||
'sessions',
|
'session',
|
||||||
'users',
|
'user',
|
||||||
'system_metadata',
|
'system_metadata',
|
||||||
'tags',
|
'tag',
|
||||||
];
|
];
|
||||||
|
|
||||||
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_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
|
||||||
},
|
},
|
||||||
|
|
||||||
setPersonThumbnail: async (personId: string) => {
|
setPersonThumbnail: async (personId: string) => {
|
||||||
|
|||||||
23
i18n/en.json
23
i18n/en.json
@@ -166,6 +166,20 @@
|
|||||||
"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",
|
||||||
@@ -359,10 +373,12 @@
|
|||||||
"admin_password": "Admin Password",
|
"admin_password": "Admin Password",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
|
"advanced_settings_beta_timeline_subtitle": "Try the new app experience.",
|
||||||
|
"advanced_settings_beta_timeline_title": "Beta Timeline",
|
||||||
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
||||||
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
||||||
"advanced_settings_log_level_title": "Log level: {level}",
|
"advanced_settings_log_level_title": "Log level: {level}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
|
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from local assets. Activate this setting to load remote images instead.",
|
||||||
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
"advanced_settings_prefer_remote_title": "Prefer remote images",
|
||||||
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||||
@@ -735,6 +751,7 @@
|
|||||||
"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",
|
||||||
@@ -1132,6 +1149,7 @@
|
|||||||
"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",
|
||||||
@@ -1501,6 +1519,7 @@
|
|||||||
"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",
|
||||||
@@ -1886,6 +1905,7 @@
|
|||||||
"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",
|
||||||
@@ -1922,6 +1942,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ 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://localhost:{port}/ping", timeout=2)
|
response = requests.get(f"http://{host}:{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)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.29.3"
|
"flutter": "3.32.6"
|
||||||
}
|
}
|
||||||
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.29.3",
|
"dart.flutterSdkPath": ".fvm/versions/3.32.6",
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.fvm": true
|
"**/.fvm": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,6 +66,20 @@ 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'
|
||||||
|
|||||||
5
mobile/android/app/src/beta/AndroidManifest.xml
Normal file
5
mobile/android/app/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ 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 {
|
||||||
@@ -100,7 +101,8 @@ 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
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds)
|
val orientation = pigeonVar_list[8] as Long
|
||||||
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -113,6 +115,7 @@ data class PlatformAsset (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
|
orientation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ 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
|
||||||
@@ -74,6 +75,8 @@ 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()
|
||||||
@@ -101,6 +104,7 @@ 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,
|
||||||
@@ -110,7 +114,8 @@ 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))
|
||||||
}
|
}
|
||||||
|
|||||||
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
File diff suppressed because one or more lines are too long
1
mobile/drift_schemas/main/drift_schema_v2.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v2.json
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -5,34 +5,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "80.0.0"
|
version: "82.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
version: "7.4.5"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: analyzer_plugin
|
name: analyzer_plugin
|
||||||
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
|
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.13.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.7.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: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.4"
|
||||||
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: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
|
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0+7.3.0"
|
version: "1.0.0+7.4.5"
|
||||||
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: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.1.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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_config
|
name: package_config
|
||||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.2.0"
|
||||||
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: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.6"
|
||||||
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: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "15.0.2"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
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-0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
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
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
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"
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ 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
|
||||||
@@ -150,6 +151,7 @@ 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,
|
||||||
@@ -159,7 +161,8 @@ 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?] {
|
||||||
@@ -172,6 +175,7 @@ struct PlatformAsset: Hashable {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
|
orientation,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +170,8 @@ 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
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
148
mobile/ios/WidgetExtension/ImageEntry.swift
Normal file
148
mobile/ios/WidgetExtension/ImageEntry.swift
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,23 +1,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
struct ImageEntry: TimelineEntry {
|
extension Image {
|
||||||
let date: Date
|
@ViewBuilder
|
||||||
var image: UIImage?
|
func tintedWidgetImageModifier() -> some View {
|
||||||
var subtitle: String? = nil
|
if #available(iOS 18.0, *) {
|
||||||
var error: WidgetError? = nil
|
self
|
||||||
var deepLink: URL? = nil
|
.widgetAccentedRenderingMode(.accentedDesaturated)
|
||||||
|
} else {
|
||||||
// Resizes the stored image to a maximum width of 450 pixels
|
self
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +20,8 @@ struct ImmichWidgetView: View {
|
|||||||
if entry.image == nil {
|
if entry.image == nil {
|
||||||
VStack {
|
VStack {
|
||||||
Image("LaunchImage")
|
Image("LaunchImage")
|
||||||
Text(entry.error?.errorDescription ?? "")
|
.tintedWidgetImageModifier()
|
||||||
|
Text(entry.metadata.error?.errorDescription ?? "")
|
||||||
.minimumScaleFactor(0.25)
|
.minimumScaleFactor(0.25)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -40,11 +32,13 @@ 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.subtitle {
|
if let subtitle = entry.metadata.subtitle {
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
@@ -55,7 +49,7 @@ struct ImmichWidgetView: View {
|
|||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
}
|
}
|
||||||
.widgetURL(entry.deepLink)
|
.widgetURL(entry.metadata.deepLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +64,9 @@ struct ImmichWidgetView: View {
|
|||||||
ImageEntry(
|
ImageEntry(
|
||||||
date: date,
|
date: date,
|
||||||
image: UIImage(named: "ImmichLogo"),
|
image: UIImage(named: "ImmichLogo"),
|
||||||
subtitle: "1 year ago"
|
metadata: EntryMetadata(
|
||||||
|
subtitle: "1 year ago"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
enum WidgetError: Error {
|
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -23,15 +29,9 @@ extension WidgetError: LocalizedError {
|
|||||||
|
|
||||||
case .albumNotFound:
|
case .albumNotFound:
|
||||||
return "Album not found"
|
return "Album not found"
|
||||||
|
|
||||||
case .invalidURL:
|
|
||||||
return "An invalid URL was used"
|
|
||||||
|
|
||||||
case .invalidImage:
|
|
||||||
return "An invalid image was received"
|
|
||||||
|
|
||||||
default:
|
case .noAssetsAvailable:
|
||||||
return "An unknown error occured"
|
return "No assets available"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,16 +46,17 @@ enum AssetType: String, Codable {
|
|||||||
struct Asset: Codable {
|
struct Asset: Codable {
|
||||||
let id: String
|
let id: String
|
||||||
let type: AssetType
|
let type: AssetType
|
||||||
|
|
||||||
var deepLink: URL? {
|
var deepLink: URL? {
|
||||||
return URL(string: "immich://asset?id=\(id)")
|
return URL(string: "immich://asset?id=\(id)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchFilters: Codable {
|
struct SearchFilter: Codable {
|
||||||
var type: AssetType = .image
|
var type = AssetType.image
|
||||||
let size: Int
|
var size = 1
|
||||||
var albumIds: [String] = []
|
var albumIds: [String] = []
|
||||||
|
var isFavorite: Bool? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoryResult: Codable {
|
struct MemoryResult: Codable {
|
||||||
@@ -70,9 +71,34 @@ struct MemoryResult: Codable {
|
|||||||
let data: MemoryData
|
let data: MemoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Album: Codable {
|
struct Album: Codable, Equatable {
|
||||||
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
|
||||||
@@ -86,7 +112,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: "group.app.immich.share"),
|
guard let defaults = UserDefaults(suiteName: IMMICH_SHARE_GROUP),
|
||||||
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 {
|
||||||
@@ -130,7 +156,8 @@ class ImmichAPI {
|
|||||||
return components?.url
|
return components?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSearchResults(with filters: SearchFilters) async throws
|
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||||
|
async throws
|
||||||
-> [Asset]
|
-> [Asset]
|
||||||
{
|
{
|
||||||
// get URL
|
// get URL
|
||||||
@@ -176,7 +203,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(WidgetError) -> UIImage {
|
func fetchImage(asset: Asset) async throws(FetchError) -> 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"
|
||||||
|
|
||||||
@@ -189,18 +216,25 @@ class ImmichAPI {
|
|||||||
else {
|
else {
|
||||||
throw .invalidURL
|
throw .invalidURL
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
|
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil)
|
||||||
|
else {
|
||||||
throw .invalidURL
|
throw .invalidURL
|
||||||
}
|
}
|
||||||
|
|
||||||
let decodeOptions: [NSString: Any] = [
|
let decodeOptions: [NSString: Any] = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
kCGImageSourceThumbnailMaxPixelSize: 512,
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||||
]
|
]
|
||||||
|
|
||||||
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
|
guard
|
||||||
|
let thumbnail = CGImageSourceCreateThumbnailAtIndex(
|
||||||
|
imageSource,
|
||||||
|
0,
|
||||||
|
decodeOptions as CFDictionary
|
||||||
|
)
|
||||||
|
else {
|
||||||
throw .fetchFailed
|
throw .fetchFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
<!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>
|
||||||
|
|||||||
@@ -7,14 +7,17 @@
|
|||||||
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(width: width, height: CGFloat(ceil(width/size.width * size.height)))
|
let canvas = CGSize(
|
||||||
let format = imageRendererFormat
|
width: width,
|
||||||
format.opaque = isOpaque
|
height: CGFloat(ceil(width / size.width * size.height))
|
||||||
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
)
|
||||||
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
let format = imageRendererFormat
|
||||||
}
|
format.opaque = isOpaque
|
||||||
|
return UIGraphicsImageRenderer(size: canvas, format: format).image {
|
||||||
|
_ in draw(in: CGRect(origin: .zero, size: canvas))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,28 +19,31 @@ 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(ImageEntry(date: Date(), image: nil, error: .noLogin))
|
completion(
|
||||||
|
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(date: Date(), image: nil, error: .fetchFailed))
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||||
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 }),
|
||||||
var entry = try? await buildEntry(
|
let entry = try? await ImageEntry.build(
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -48,26 +51,17 @@ struct ImmichMemoryProvider: TimelineProvider {
|
|||||||
|
|
||||||
// fallback to random image
|
// fallback to random image
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults().first,
|
||||||
with: SearchFilters(size: 1)
|
let imageEntry = try? await ImageEntry.build(
|
||||||
).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(date: Date(), image: nil, error: .fetchFailed))
|
completion(ImageEntry.handleError(for: cacheKey).entries.first!)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imageEntry.resize()
|
|
||||||
completion(imageEntry)
|
completion(imageEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,9 +74,12 @@ 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 {
|
||||||
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
completion(
|
||||||
completion(Timeline(entries: entries, policy: .atEnd))
|
ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +92,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 buildEntry(
|
try? await ImageEntry.build(
|
||||||
api: api,
|
api: api,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
dateOffset: totalAssets,
|
dateOffset: totalAssets,
|
||||||
@@ -120,25 +117,32 @@ 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 {
|
||||||
entries.append(
|
// this must be a do/catch since we need to
|
||||||
contentsOf: (try? await generateRandomEntries(
|
// distinguish between a network fail and an empty search
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we fail to fetch images, we still want to add an entry
|
// cache the last image
|
||||||
// with a nil image and an error
|
try? entries.last!.cache(for: cacheKey)
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ 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] {
|
||||||
// use cached albums to search
|
return await suggestedEntities().filter {
|
||||||
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 throws -> [Album] {
|
func suggestedEntities() async -> [Album] {
|
||||||
var albums = (try? await AlbumCache.shared.getAlbums(refresh: true)) ?? []
|
let albums = (try? await AlbumCache.shared.getAlbums()) ?? []
|
||||||
albums.insert(NO_ALBUM, at: 0)
|
|
||||||
|
|
||||||
return albums
|
let options =
|
||||||
|
[
|
||||||
|
NONE,
|
||||||
|
FAVORITES,
|
||||||
|
] + albums
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +36,6 @@ 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 {
|
||||||
@@ -45,7 +44,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
|
|||||||
|
|
||||||
@Parameter(title: "Album")
|
@Parameter(title: "Album")
|
||||||
var album: Album?
|
var album: Album?
|
||||||
|
|
||||||
@Parameter(title: "Show Album Name", default: false)
|
@Parameter(title: "Show Album Name", default: false)
|
||||||
var showAlbumName: Bool
|
var showAlbumName: Bool
|
||||||
}
|
}
|
||||||
@@ -54,7 +53,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(), image: nil)
|
ImageEntry(date: Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshot(
|
func snapshot(
|
||||||
@@ -63,30 +62,26 @@ 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(date: Date(), image: nil, error: .noLogin)
|
return ImageEntry.handleError(for: cacheKey, error: .noLogin).entries
|
||||||
|
.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let randomImage = try? await api.fetchSearchResults(
|
let randomImage = try? await api.fetchSearchResults(
|
||||||
with: SearchFilters(size: 1)
|
with: Album.NONE.filter
|
||||||
).first
|
).first,
|
||||||
else {
|
let entry = try? await ImageEntry.build(
|
||||||
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(date: Date(), image: nil, error: .fetchFailed)
|
return ImageEntry.handleError(for: cacheKey).entries.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.resize()
|
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,50 +94,41 @@ 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 {
|
||||||
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
return ImageEntry.handleError(for: cacheKey, error: .noLogin)
|
||||||
return Timeline(entries: entries, policy: .atEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nil if album is NONE or nil
|
// build entries
|
||||||
let albumId =
|
// this must be a do/catch since we need to
|
||||||
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
// distinguish between a network fail and an empty search
|
||||||
var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
|
do {
|
||||||
|
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,
|
||||||
albumId: albumId,
|
filter: album.filter,
|
||||||
subtitle: configuration.showAlbumName ? albumName : nil
|
subtitle: configuration.showAlbumName ? albumName : nil
|
||||||
))
|
)
|
||||||
?? []
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we fail to fetch images, we still want to add an entry with a nil image and an error
|
// Load or save a cached asset for when network conditions are bad
|
||||||
if entries.count == 0 {
|
if search.count == 0 {
|
||||||
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
return ImageEntry.handleError(for: cacheKey, error: .noAssetsAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(contentsOf: search)
|
||||||
|
} catch {
|
||||||
|
return ImageEntry.handleError(for: cacheKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize all images to something that can be stored by iOS
|
// cache the last image
|
||||||
for i in entries.indices {
|
try? entries.last!.cache(for: cacheKey)
|
||||||
entries[i].resize()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Timeline(entries: entries, policy: .atEnd)
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ enum AlbumUserRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model for an album stored in the server
|
// Model for an album stored in the server
|
||||||
class Album {
|
class RemoteAlbum {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
@@ -24,7 +24,7 @@ class Album {
|
|||||||
final int assetCount;
|
final int assetCount;
|
||||||
final String ownerName;
|
final String ownerName;
|
||||||
|
|
||||||
const Album({
|
const RemoteAlbum({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.ownerId,
|
required this.ownerId,
|
||||||
@@ -57,7 +57,7 @@ class Album {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! Album) return false;
|
if (other is! RemoteAlbum) 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,4 +86,32 @@ class Album {
|
|||||||
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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,
|
||||||
@@ -36,16 +37,20 @@ 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;
|
||||||
|
|
||||||
double? get aspectRatio {
|
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||||
if (width != null && height != null && height! > 0) {
|
|
||||||
return width! / height!;
|
Duration get duration {
|
||||||
|
final durationInSeconds = this.durationInSeconds;
|
||||||
|
if (durationInSeconds != null) {
|
||||||
|
return Duration(seconds: durationInSeconds);
|
||||||
}
|
}
|
||||||
return null;
|
return const Duration();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasRemote =>
|
bool get hasRemote =>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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,
|
||||||
@@ -16,6 +17,8 @@ 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
|
||||||
@@ -38,6 +41,7 @@ class LocalAsset extends BaseAsset {
|
|||||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||||
remoteId: ${remoteId ?? "<NA>"}
|
remoteId: ${remoteId ?? "<NA>"}
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
|
orientation: $orientation,
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +49,12 @@ 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 && remoteId == other.remoteId;
|
return super == other && id == other.id && orientation == other.orientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
|
int get hashCode =>
|
||||||
|
super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
|
||||||
|
|
||||||
LocalAsset copyWith({
|
LocalAsset copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -63,6 +68,7 @@ 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,
|
||||||
@@ -76,6 +82,7 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ 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
|
||||||
@@ -65,7 +66,6 @@ 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,6 +94,7 @@ 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,
|
||||||
@@ -110,6 +111,7 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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),
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ enum StoreKey<T> {
|
|||||||
manageLocalMediaAndroid<bool>._(137),
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000);
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
|
betaPromptShown<bool>._(1001),
|
||||||
|
betaTimeline<bool>._(1002);
|
||||||
|
|
||||||
const StoreKey._(this.id);
|
const StoreKey._(this.id);
|
||||||
final int id;
|
final int id;
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
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"),
|
||||||
@@ -31,7 +38,45 @@ enum AvatarColor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserPreferences {
|
class Onboarding {
|
||||||
|
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;
|
||||||
@@ -41,7 +86,7 @@ class UserPreferences {
|
|||||||
final AvatarColor userAvatarColor;
|
final AvatarColor userAvatarColor;
|
||||||
final bool showSupportBadge;
|
final bool showSupportBadge;
|
||||||
|
|
||||||
const UserPreferences({
|
const Preferences({
|
||||||
this.foldersEnabled = false,
|
this.foldersEnabled = false,
|
||||||
this.memoriesEnabled = true,
|
this.memoriesEnabled = true,
|
||||||
this.peopleEnabled = true,
|
this.peopleEnabled = true,
|
||||||
@@ -52,7 +97,7 @@ class UserPreferences {
|
|||||||
this.showSupportBadge = true,
|
this.showSupportBadge = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
UserPreferences copyWith({
|
Preferences copyWith({
|
||||||
bool? foldersEnabled,
|
bool? foldersEnabled,
|
||||||
bool? memoriesEnabled,
|
bool? memoriesEnabled,
|
||||||
bool? peopleEnabled,
|
bool? peopleEnabled,
|
||||||
@@ -62,7 +107,7 @@ class UserPreferences {
|
|||||||
AvatarColor? userAvatarColor,
|
AvatarColor? userAvatarColor,
|
||||||
bool? showSupportBadge,
|
bool? showSupportBadge,
|
||||||
}) {
|
}) {
|
||||||
return UserPreferences(
|
return Preferences(
|
||||||
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
|
foldersEnabled: foldersEnabled ?? this.foldersEnabled,
|
||||||
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
|
memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
|
||||||
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
|
peopleEnabled: peopleEnabled ?? this.peopleEnabled,
|
||||||
@@ -87,8 +132,8 @@ class UserPreferences {
|
|||||||
return preferences;
|
return preferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory UserPreferences.fromMap(Map<String, Object?> map) {
|
factory Preferences.fromMap(Map<String, Object?> map) {
|
||||||
return UserPreferences(
|
return Preferences(
|
||||||
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,
|
||||||
@@ -102,4 +147,173 @@ class UserPreferences {
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ 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;
|
||||||
@@ -21,10 +25,44 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||||
if (asset is LocalAsset || asset is! RemoteAsset) {
|
if (!asset.hasRemote) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _remoteAssetRepository.getExif(asset.id);
|
final 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
17
mobile/lib/domain/services/local_album.service.dart
Normal file
17
mobile/lib/domain/services/local_album.service.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -359,6 +359,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,43 @@
|
|||||||
|
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);
|
const RemoteAlbumService(this._repository, this._albumApiRepository);
|
||||||
|
|
||||||
Future<List<Album>> getAll() {
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
|
return _repository.watchAlbum(albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<RemoteAlbum>> getAll() {
|
||||||
return _repository.getAll();
|
return _repository.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Album> sortAlbums(
|
List<RemoteAlbum> sortAlbums(
|
||||||
List<Album> albums,
|
List<RemoteAlbum> albums,
|
||||||
RemoteAlbumSortMode sortMode, {
|
RemoteAlbumSortMode sortMode, {
|
||||||
bool isReverse = false,
|
bool isReverse = false,
|
||||||
}) {
|
}) {
|
||||||
return sortMode.sortFn(albums, isReverse);
|
return sortMode.sortFn(albums, isReverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Album> searchAlbums(
|
List<RemoteAlbum> searchAlbums(
|
||||||
List<Album> albums,
|
List<RemoteAlbum> 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<Album> filtered = albums;
|
List<RemoteAlbum> filtered = albums;
|
||||||
|
|
||||||
// Apply text search filter
|
// Apply text search filter
|
||||||
if (query.isNotEmpty) {
|
if (query.isNotEmpty) {
|
||||||
@@ -57,4 +67,84 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class StoreService {
|
|||||||
await _storeRepository.deleteAll();
|
await _storeRepository.deleteAll();
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreKeyNotFoundException implements Exception {
|
class StoreKeyNotFoundException implements Exception {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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';
|
||||||
|
|
||||||
@@ -23,7 +24,65 @@ class SyncStreamService {
|
|||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> sync() => _syncApiRepository.streamChanges(_handleEvents);
|
Future<void> sync() {
|
||||||
|
_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 = [];
|
||||||
@@ -173,6 +232,14 @@ 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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.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/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
@@ -18,6 +19,11 @@ 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;
|
||||||
@@ -31,109 +37,105 @@ 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(
|
TimelineService main(List<String> timelineUsers) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.main(timelineUsers, groupBy));
|
||||||
.getMainBucketAssets(timelineUsers, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchMainBucket(
|
|
||||||
timelineUsers,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService localAlbum({required String albumId}) => TimelineService(
|
TimelineService localAlbum({required String albumId}) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.localAlbum(albumId, groupBy));
|
||||||
.getLocalAlbumBucketAssets(albumId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchLocalAlbumBucket(
|
|
||||||
albumId,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService remoteAlbum({required String albumId}) => TimelineService(
|
TimelineService remoteAlbum({required String albumId}) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy));
|
||||||
.getRemoteAlbumBucketAssets(albumId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchRemoteAlbumBucket(
|
|
||||||
albumId,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService remoteAssets(List<String> timelineUsers) => TimelineService(
|
TimelineService remoteAssets(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.remote(userId, groupBy));
|
||||||
.getRemoteBucketAssets(timelineUsers, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchRemoteBucket(
|
|
||||||
timelineUsers,
|
|
||||||
groupBy: GroupAssetsBy.month,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService favorite(String userId) => TimelineService(
|
TimelineService favorite(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.favorite(userId, groupBy));
|
||||||
.getFavoriteBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchFavoriteBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService trash(String userId) => TimelineService(
|
TimelineService trash(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.trash(userId, groupBy));
|
||||||
.getTrashBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService archive(String userId) => TimelineService(
|
TimelineService archive(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.archived(userId, groupBy));
|
||||||
.getArchiveBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
|
||||||
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService lockedFolder(String userId) => TimelineService(
|
TimelineService lockedFolder(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.locked(userId, groupBy));
|
||||||
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
|
|
||||||
userId,
|
|
||||||
groupBy: groupBy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
TimelineService video(String userId) => TimelineService(
|
TimelineService video(String userId) =>
|
||||||
assetSource: (offset, count) => _timelineRepository
|
TimelineService(_timelineRepository.video(userId, groupBy));
|
||||||
.getVideoBucketAssets(userId, offset: offset, count: count),
|
|
||||||
bucketSource: () =>
|
TimelineService place(String place) =>
|
||||||
_timelineRepository.watchVideoBucket(userId, groupBy: groupBy),
|
TimelineService(_timelineRepository.place(place, groupBy));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
final TimelineAssetSource _assetSource;
|
final TimelineAssetSource _assetSource;
|
||||||
final TimelineBucketSource _bucketSource;
|
final TimelineBucketSource _bucketSource;
|
||||||
int _totalAssets = 0;
|
|
||||||
int get totalAssets => _totalAssets;
|
|
||||||
|
|
||||||
TimelineService({
|
|
||||||
required TimelineAssetSource assetSource,
|
|
||||||
required TimelineBucketSource bucketSource,
|
|
||||||
}) : _assetSource = assetSource,
|
|
||||||
_bucketSource = bucketSource {
|
|
||||||
_bucketSubscription = _bucketSource().listen((buckets) {
|
|
||||||
_totalAssets =
|
|
||||||
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
|
||||||
unawaited(_reloadBucket());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final AsyncMutex _mutex = AsyncMutex();
|
final AsyncMutex _mutex = AsyncMutex();
|
||||||
int _bufferOffset = 0;
|
int _bufferOffset = 0;
|
||||||
List<BaseAsset> _buffer = [];
|
List<BaseAsset> _buffer = [];
|
||||||
StreamSubscription? _bucketSubscription;
|
StreamSubscription? _bucketSubscription;
|
||||||
|
|
||||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
int _totalAssets = 0;
|
||||||
|
int get totalAssets => _totalAssets;
|
||||||
|
|
||||||
Future<void> _reloadBucket() => _mutex.run(() async {
|
TimelineService(TimelineQuery query)
|
||||||
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
: this._(
|
||||||
|
assetSource: query.assetSource,
|
||||||
|
bucketSource: query.bucketSource,
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService._({
|
||||||
|
required TimelineAssetSource assetSource,
|
||||||
|
required TimelineBucketSource bucketSource,
|
||||||
|
}) : _assetSource = assetSource,
|
||||||
|
_bucketSource = bucketSource {
|
||||||
|
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||||
|
_mutex.run(() async {
|
||||||
|
final totalAssets =
|
||||||
|
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_buffer = await _assetSource(offset, count);
|
||||||
|
_bufferOffset = offset;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('database has been locked')) {
|
||||||
|
debugPrint(
|
||||||
|
"TimelineService::loadAssets - Database locked, returning cached assets",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the state's total assets count only after the buffer is reloaded
|
||||||
|
_totalAssets = totalAssets;
|
||||||
EventStream.shared.emit(const TimelineReloadEvent());
|
EventStream.shared.emit(const TimelineReloadEvent());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||||
|
|
||||||
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));
|
||||||
@@ -163,18 +165,34 @@ class TimelineService {
|
|||||||
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
|
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
|
||||||
);
|
);
|
||||||
|
|
||||||
final assets = await _assetSource(start, len);
|
try {
|
||||||
_buffer = assets;
|
_buffer = await _assetSource(start, len);
|
||||||
_bufferOffset = start;
|
_bufferOffset = start;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('database has been locked') &&
|
||||||
|
_buffer.isNotEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
"TimelineService::loadAssets - Database locked, returning cached assets",
|
||||||
|
);
|
||||||
|
if (hasRange(index, count)) {
|
||||||
|
return getAssets(index, count);
|
||||||
|
}
|
||||||
|
return <BaseAsset>[];
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
return getAssets(index, count);
|
return getAssets(index, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasRange(int index, int count) =>
|
bool hasRange(int index, int count) =>
|
||||||
index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
|
index >= 0 &&
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
@@ -184,11 +202,16 @@ 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, 5));
|
_mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index)));
|
||||||
|
|
||||||
|
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('TimelineService::getAsset Index out of range');
|
throw RangeError(
|
||||||
|
'TimelineService::getAsset Index $index not in buffer range [$_bufferOffset, ${_bufferOffset + _buffer.length})',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return _buffer.elementAt(index - _bufferOffset);
|
return _buffer.elementAt(index - _bufferOffset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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;
|
||||||
|
|
||||||
@@ -20,6 +21,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,4 +79,19 @@ 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
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}) {
|
||||||
@@ -35,3 +38,56 @@ 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,3 +3,15 @@ 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ class FastScrollPhysics extends ScrollPhysics {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
SpringDescription get spring => const SpringDescription(
|
SpringDescription get spring => const SpringDescription(
|
||||||
mass: 40,
|
mass: 1,
|
||||||
stiffness: 100,
|
stiffness: 402.49984375,
|
||||||
damping: 1,
|
damping: 40,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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: 80,
|
mass: 1,
|
||||||
stiffness: 100,
|
stiffness: 1601.2499609375,
|
||||||
damping: 1,
|
damping: 80,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
|||||||
// 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};
|
||||||
}
|
}
|
||||||
@@ -31,5 +33,6 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
|||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
|
orientation: orientation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder
|
|||||||
required String id,
|
required String id,
|
||||||
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({
|
||||||
@@ -33,6 +34,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder
|
|||||||
i0.Value<String> id,
|
i0.Value<String> id,
|
||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
|
i0.Value<int> orientation,
|
||||||
});
|
});
|
||||||
|
|
||||||
class $$LocalAssetEntityTableFilterComposer
|
class $$LocalAssetEntityTableFilterComposer
|
||||||
@@ -76,6 +78,10 @@ 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(
|
||||||
|
column: $table.orientation,
|
||||||
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableOrderingComposer
|
class $$LocalAssetEntityTableOrderingComposer
|
||||||
@@ -120,6 +126,10 @@ class $$LocalAssetEntityTableOrderingComposer
|
|||||||
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
|
||||||
column: $table.isFavorite,
|
column: $table.isFavorite,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<int> get orientation => $composableBuilder(
|
||||||
|
column: $table.orientation,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableAnnotationComposer
|
class $$LocalAssetEntityTableAnnotationComposer
|
||||||
@@ -160,6 +170,9 @@ 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(
|
||||||
|
column: $table.orientation, builder: (column) => column);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
||||||
@@ -201,6 +214,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<String> id = const i0.Value.absent(),
|
i0.Value<String> id = 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,
|
||||||
@@ -213,6 +227,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
id: id,
|
id: id,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
orientation: orientation,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -225,6 +240,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
required String id,
|
required String id,
|
||||||
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,
|
||||||
@@ -237,6 +253,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
id: id,
|
id: id,
|
||||||
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), i0.BaseReferences(db, table, e)))
|
||||||
@@ -337,6 +354,14 @@ 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,
|
||||||
@@ -348,7 +373,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
id,
|
id,
|
||||||
checksum,
|
checksum,
|
||||||
isFavorite
|
isFavorite,
|
||||||
|
orientation
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -404,6 +430,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +467,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
.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'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +497,7 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
final String id;
|
final String id;
|
||||||
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,
|
||||||
@@ -473,7 +508,8 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
this.durationInSeconds,
|
this.durationInSeconds,
|
||||||
required this.id,
|
required this.id,
|
||||||
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>{};
|
||||||
@@ -498,6 +534,7 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,6 +553,7 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
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
|
||||||
@@ -533,6 +571,7 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +585,8 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||||
String? id,
|
String? id,
|
||||||
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,
|
||||||
@@ -560,6 +600,7 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
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(
|
||||||
@@ -576,6 +617,8 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,14 +634,15 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
..write('durationInSeconds: $durationInSeconds, ')
|
..write('durationInSeconds: $durationInSeconds, ')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..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);
|
height, durationInSeconds, id, checksum, isFavorite, orientation);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
@@ -612,7 +656,8 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
other.durationInSeconds == this.durationInSeconds &&
|
other.durationInSeconds == this.durationInSeconds &&
|
||||||
other.id == this.id &&
|
other.id == this.id &&
|
||||||
other.checksum == this.checksum &&
|
other.checksum == this.checksum &&
|
||||||
other.isFavorite == this.isFavorite);
|
other.isFavorite == this.isFavorite &&
|
||||||
|
other.orientation == this.orientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalAssetEntityCompanion
|
class LocalAssetEntityCompanion
|
||||||
@@ -627,6 +672,7 @@ class LocalAssetEntityCompanion
|
|||||||
final i0.Value<String> id;
|
final i0.Value<String> id;
|
||||||
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(),
|
||||||
@@ -638,6 +684,7 @@ class LocalAssetEntityCompanion
|
|||||||
this.id = const i0.Value.absent(),
|
this.id = 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,
|
||||||
@@ -650,6 +697,7 @@ class LocalAssetEntityCompanion
|
|||||||
required String id,
|
required String id,
|
||||||
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);
|
||||||
@@ -664,6 +712,7 @@ class LocalAssetEntityCompanion
|
|||||||
i0.Expression<String>? id,
|
i0.Expression<String>? id,
|
||||||
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,
|
||||||
@@ -676,6 +725,7 @@ class LocalAssetEntityCompanion
|
|||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,7 +739,8 @@ class LocalAssetEntityCompanion
|
|||||||
i0.Value<int?>? durationInSeconds,
|
i0.Value<int?>? durationInSeconds,
|
||||||
i0.Value<String>? id,
|
i0.Value<String>? id,
|
||||||
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,
|
||||||
@@ -701,6 +752,7 @@ class LocalAssetEntityCompanion
|
|||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
checksum: checksum ?? this.checksum,
|
checksum: checksum ?? this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
orientation: orientation ?? this.orientation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,6 +790,9 @@ class LocalAssetEntityCompanion
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +808,8 @@ class LocalAssetEntityCompanion
|
|||||||
..write('durationInSeconds: $durationInSeconds, ')
|
..write('durationInSeconds: $durationInSeconds, ')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite')
|
..write('isFavorite: $isFavorite, ')
|
||||||
|
..write('orientation: $orientation')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ 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
|
LEFT JOIN
|
||||||
@@ -37,7 +39,9 @@ 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
|
LEFT JOIN
|
||||||
|
|||||||
@@ -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 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 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, 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}',
|
||||||
variables: [
|
variables: [
|
||||||
for (var $ in var1) i0.Variable<String>($),
|
for (var $ in var1) i0.Variable<String>($),
|
||||||
...generatedlimit.introducedVariables
|
...generatedlimit.introducedVariables
|
||||||
@@ -42,6 +42,8 @@ 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'),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +89,8 @@ 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,
|
||||||
@@ -101,6 +105,8 @@ class MergedAssetResult {
|
|||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.checksum,
|
this.checksum,
|
||||||
this.ownerId,
|
this.ownerId,
|
||||||
|
this.livePhotoVideoId,
|
||||||
|
required this.orientation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ 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
|
||||||
@@ -51,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||||||
width: width,
|
width: width,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
localId: null,
|
localId: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder
|
|||||||
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
|
||||||
@@ -45,6 +46,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder
|
|||||||
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,6 +136,10 @@ 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,
|
||||||
@@ -217,6 +223,10 @@ 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));
|
||||||
@@ -292,6 +302,9 @@ 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);
|
||||||
@@ -358,6 +371,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
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(
|
||||||
@@ -375,6 +389,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
localDateTime: localDateTime,
|
localDateTime: localDateTime,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
@@ -392,6 +407,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
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(
|
||||||
@@ -409,6 +425,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager<
|
|||||||
localDateTime: localDateTime,
|
localDateTime: localDateTime,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
deletedAt: deletedAt,
|
deletedAt: deletedAt,
|
||||||
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
@@ -573,6 +590,12 @@ 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,
|
||||||
@@ -595,6 +618,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
|
|||||||
localDateTime,
|
localDateTime,
|
||||||
thumbHash,
|
thumbHash,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
|
livePhotoVideoId,
|
||||||
visibility
|
visibility
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
@@ -673,6 +697,12 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,6 +742,9 @@ 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'])!),
|
||||||
@@ -750,6 +783,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
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,
|
||||||
@@ -766,6 +800,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
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) {
|
||||||
@@ -799,6 +834,9 @@ 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));
|
||||||
@@ -825,6 +863,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
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'])),
|
||||||
);
|
);
|
||||||
@@ -848,6 +887,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
'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)),
|
||||||
};
|
};
|
||||||
@@ -868,6 +908,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
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,
|
||||||
@@ -887,6 +928,9 @@ 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) {
|
||||||
@@ -910,6 +954,9 @@ 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,
|
||||||
);
|
);
|
||||||
@@ -932,6 +979,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
..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();
|
||||||
@@ -953,6 +1001,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
localDateTime,
|
localDateTime,
|
||||||
thumbHash,
|
thumbHash,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
|
livePhotoVideoId,
|
||||||
visibility);
|
visibility);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -972,6 +1021,7 @@ class RemoteAssetEntityData extends i0.DataClass
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,6 +1041,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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(),
|
||||||
@@ -1007,6 +1058,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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({
|
||||||
@@ -1024,6 +1076,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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),
|
||||||
@@ -1046,6 +1099,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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({
|
||||||
@@ -1063,6 +1117,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1082,6 +1137,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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,
|
||||||
@@ -1098,6 +1154,7 @@ class RemoteAssetEntityCompanion
|
|||||||
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1148,6 +1205,9 @@ 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
|
||||||
@@ -1173,6 +1233,7 @@ class RemoteAssetEntityCompanion
|
|||||||
..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();
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ 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};
|
Set<Column> get primaryKey => {userId, key};
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonTypeConverter2<UserPreferences, String, Object?>
|
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?>
|
||||||
userPreferenceConverter = TypeConverter.json2(
|
userMetadataConverter = TypeConverter.jsonb(
|
||||||
fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
|
fromJson: (json) => json as Map<String, Object?>,
|
||||||
toJson: (pref) => pref.toMap(),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ 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 i3;
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
|
||||||
as i4;
|
as i4;
|
||||||
import 'package:drift/internal/modular.dart' as i5;
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
import 'package:drift/internal/modular.dart' as i6;
|
||||||
|
|
||||||
typedef $$UserMetadataEntityTableCreateCompanionBuilder
|
typedef $$UserMetadataEntityTableCreateCompanionBuilder
|
||||||
= i1.UserMetadataEntityCompanion Function({
|
= i1.UserMetadataEntityCompanion Function({
|
||||||
required String userId,
|
required String userId,
|
||||||
required i2.UserPreferences preferences,
|
required i2.UserMetadataKey key,
|
||||||
|
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.UserPreferences> preferences,
|
i0.Value<i2.UserMetadataKey> key,
|
||||||
|
i0.Value<Map<String, Object?>> value,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
|
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
|
||||||
@@ -28,26 +31,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
|
|||||||
$$UserMetadataEntityTableReferences(
|
$$UserMetadataEntityTableReferences(
|
||||||
super.$_db, super.$_table, super.$_typedResult);
|
super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
|
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
|
||||||
i5.ReadDatabaseContainer(db)
|
i6.ReadDatabaseContainer(db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity')
|
.resultSet<i5.$UserEntityTable>('user_entity')
|
||||||
.createAlias(i0.$_aliasNameGenerator(
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
i5.ReadDatabaseContainer(db)
|
i6.ReadDatabaseContainer(db)
|
||||||
.resultSet<i1.$UserMetadataEntityTable>(
|
.resultSet<i1.$UserMetadataEntityTable>(
|
||||||
'user_metadata_entity')
|
'user_metadata_entity')
|
||||||
.userId,
|
.userId,
|
||||||
i5.ReadDatabaseContainer(db)
|
i6.ReadDatabaseContainer(db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity')
|
.resultSet<i5.$UserEntityTable>('user_entity')
|
||||||
.id));
|
.id));
|
||||||
|
|
||||||
i4.$$UserEntityTableProcessedTableManager get userId {
|
i5.$$UserEntityTableProcessedTableManager get userId {
|
||||||
final $_column = $_itemColumn<String>('user_id')!;
|
final $_column = $_itemColumn<String>('user_id')!;
|
||||||
|
|
||||||
final manager = i4
|
final manager = i5
|
||||||
.$$UserEntityTableTableManager(
|
.$$UserEntityTableTableManager(
|
||||||
$_db,
|
$_db,
|
||||||
i5.ReadDatabaseContainer($_db)
|
i6.ReadDatabaseContainer($_db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'))
|
.resultSet<i5.$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;
|
||||||
@@ -65,26 +68,31 @@ class $$UserMetadataEntityTableFilterComposer
|
|||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
i0.ColumnWithTypeConverterFilters<i2.UserPreferences, i2.UserPreferences,
|
i0.ColumnWithTypeConverterFilters<i2.UserMetadataKey, i2.UserMetadataKey, int>
|
||||||
String>
|
get key => $composableBuilder(
|
||||||
get preferences => $composableBuilder(
|
column: $table.key,
|
||||||
column: $table.preferences,
|
|
||||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
||||||
|
|
||||||
i4.$$UserEntityTableFilterComposer get userId {
|
i0.ColumnWithTypeConverterFilters<Map<String, Object?>, Map<String, Object>,
|
||||||
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
i3.Uint8List>
|
||||||
|
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: i5.ReadDatabaseContainer($db)
|
referencedTable: i6.ReadDatabaseContainer($db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder: (joinBuilder,
|
builder: (joinBuilder,
|
||||||
{$addJoinBuilderToRootComposer,
|
{$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
i4.$$UserEntityTableFilterComposer(
|
i5.$$UserEntityTableFilterComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
$table: i6.ReadDatabaseContainer($db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -103,24 +111,26 @@ class $$UserMetadataEntityTableOrderingComposer
|
|||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
i0.ColumnOrderings<String> get preferences => $composableBuilder(
|
i0.ColumnOrderings<int> get key => $composableBuilder(
|
||||||
column: $table.preferences,
|
column: $table.key, builder: (column) => i0.ColumnOrderings(column));
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
|
||||||
|
|
||||||
i4.$$UserEntityTableOrderingComposer get userId {
|
i0.ColumnOrderings<i3.Uint8List> get value => $composableBuilder(
|
||||||
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
column: $table.value, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i5.$$UserEntityTableOrderingComposer get userId {
|
||||||
|
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
getCurrentColumn: (t) => t.userId,
|
getCurrentColumn: (t) => t.userId,
|
||||||
referencedTable: i5.ReadDatabaseContainer($db)
|
referencedTable: i6.ReadDatabaseContainer($db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder: (joinBuilder,
|
builder: (joinBuilder,
|
||||||
{$addJoinBuilderToRootComposer,
|
{$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
i4.$$UserEntityTableOrderingComposer(
|
i5.$$UserEntityTableOrderingComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
$table: i6.ReadDatabaseContainer($db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -139,24 +149,27 @@ class $$UserMetadataEntityTableAnnotationComposer
|
|||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
|
i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> get key =>
|
||||||
get preferences => $composableBuilder(
|
$composableBuilder(column: $table.key, builder: (column) => column);
|
||||||
column: $table.preferences, builder: (column) => column);
|
|
||||||
|
|
||||||
i4.$$UserEntityTableAnnotationComposer get userId {
|
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
|
||||||
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
get value =>
|
||||||
|
$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: i5.ReadDatabaseContainer($db)
|
referencedTable: i6.ReadDatabaseContainer($db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
getReferencedColumn: (t) => t.id,
|
getReferencedColumn: (t) => t.id,
|
||||||
builder: (joinBuilder,
|
builder: (joinBuilder,
|
||||||
{$addJoinBuilderToRootComposer,
|
{$addJoinBuilderToRootComposer,
|
||||||
$removeJoinBuilderFromRootComposer}) =>
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
i4.$$UserEntityTableAnnotationComposer(
|
i5.$$UserEntityTableAnnotationComposer(
|
||||||
$db: $db,
|
$db: $db,
|
||||||
$table: i5.ReadDatabaseContainer($db)
|
$table: i6.ReadDatabaseContainer($db)
|
||||||
.resultSet<i4.$UserEntityTable>('user_entity'),
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
joinBuilder: joinBuilder,
|
joinBuilder: joinBuilder,
|
||||||
$removeJoinBuilderFromRootComposer:
|
$removeJoinBuilderFromRootComposer:
|
||||||
@@ -193,19 +206,23 @@ 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.UserPreferences> preferences = const i0.Value.absent(),
|
i0.Value<i2.UserMetadataKey> key = const i0.Value.absent(),
|
||||||
|
i0.Value<Map<String, Object?>> value = const i0.Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
i1.UserMetadataEntityCompanion(
|
i1.UserMetadataEntityCompanion(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
preferences: preferences,
|
key: key,
|
||||||
|
value: value,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
required String userId,
|
required String userId,
|
||||||
required i2.UserPreferences preferences,
|
required i2.UserMetadataKey key,
|
||||||
|
required Map<String, Object?> value,
|
||||||
}) =>
|
}) =>
|
||||||
i1.UserMetadataEntityCompanion.insert(
|
i1.UserMetadataEntityCompanion.insert(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
preferences: preferences,
|
key: key,
|
||||||
|
value: value,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
@@ -266,7 +283,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager
|
|||||||
i1.UserMetadataEntityData,
|
i1.UserMetadataEntityData,
|
||||||
i0.PrefetchHooks Function({bool userId})>;
|
i0.PrefetchHooks Function({bool userId})>;
|
||||||
|
|
||||||
class $UserMetadataEntityTable extends i3.UserMetadataEntity
|
class $UserMetadataEntityTable extends i4.UserMetadataEntity
|
||||||
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
|
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
|
||||||
@override
|
@override
|
||||||
final i0.GeneratedDatabase attachedDatabase;
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
@@ -282,14 +299,20 @@ class $UserMetadataEntityTable extends i3.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.UserPreferences, String>
|
late final i0.GeneratedColumnWithTypeConverter<i2.UserMetadataKey, int> key =
|
||||||
preferences = i0.GeneratedColumn<String>(
|
i0.GeneratedColumn<int>('key', aliasedName, false,
|
||||||
'preferences', aliasedName, false,
|
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
||||||
type: i0.DriftSqlType.string, requiredDuringInsert: true)
|
.withConverter<i2.UserMetadataKey>(
|
||||||
.withConverter<i2.UserPreferences>(
|
i1.$UserMetadataEntityTable.$converterkey);
|
||||||
i1.$UserMetadataEntityTable.$converterpreferences);
|
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [userId, preferences];
|
late final i0
|
||||||
|
.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
|
||||||
@@ -311,7 +334,7 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<i0.GeneratedColumn> get $primaryKey => {userId};
|
Set<i0.GeneratedColumn> get $primaryKey => {userId, key};
|
||||||
@override
|
@override
|
||||||
i1.UserMetadataEntityData map(Map<String, dynamic> data,
|
i1.UserMetadataEntityData map(Map<String, dynamic> data,
|
||||||
{String? tablePrefix}) {
|
{String? tablePrefix}) {
|
||||||
@@ -319,9 +342,12 @@ class $UserMetadataEntityTable extends i3.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'])!,
|
||||||
preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
|
key: i1.$UserMetadataEntityTable.$converterkey.fromSql(attachedDatabase
|
||||||
attachedDatabase.typeMapping.read(
|
.typeMapping
|
||||||
i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
|
.read(i0.DriftSqlType.int, data['${effectivePrefix}key'])!),
|
||||||
|
value: i1.$UserMetadataEntityTable.$convertervalue.fromSql(
|
||||||
|
attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.blob, data['${effectivePrefix}value'])!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +356,11 @@ class $UserMetadataEntityTable extends i3.UserMetadataEntity
|
|||||||
return $UserMetadataEntityTable(attachedDatabase, alias);
|
return $UserMetadataEntityTable(attachedDatabase, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
static i0.JsonTypeConverter2<i2.UserPreferences, String, Object?>
|
static i0.JsonTypeConverter2<i2.UserMetadataKey, int, int> $converterkey =
|
||||||
$converterpreferences = i3.userPreferenceConverter;
|
const i0.EnumIndexConverter<i2.UserMetadataKey>(
|
||||||
|
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
|
||||||
@@ -341,16 +370,21 @@ class $UserMetadataEntityTable extends i3.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.UserPreferences preferences;
|
final i2.UserMetadataKey key;
|
||||||
|
final Map<String, Object?> value;
|
||||||
const UserMetadataEntityData(
|
const UserMetadataEntityData(
|
||||||
{required this.userId, required this.preferences});
|
{required this.userId, required this.key, required this.value});
|
||||||
@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['preferences'] = i0.Variable<String>(
|
map['key'] = i0.Variable<int>(
|
||||||
i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
|
i1.$UserMetadataEntityTable.$converterkey.toSql(key));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
map['value'] = i0.Variable<i3.Uint8List>(
|
||||||
|
i1.$UserMetadataEntityTable.$convertervalue.toSql(value));
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@@ -360,8 +394,10 @@ 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']),
|
||||||
preferences: i1.$UserMetadataEntityTable.$converterpreferences
|
key: i1.$UserMetadataEntityTable.$converterkey
|
||||||
.fromJson(serializer.fromJson<Object?>(json['preferences'])),
|
.fromJson(serializer.fromJson<int>(json['key'])),
|
||||||
|
value: i1.$UserMetadataEntityTable.$convertervalue
|
||||||
|
.fromJson(serializer.fromJson<Object?>(json['value'])),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -369,24 +405,28 @@ 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),
|
||||||
'preferences': serializer.toJson<Object?>(i1
|
'key': serializer
|
||||||
.$UserMetadataEntityTable.$converterpreferences
|
.toJson<int>(i1.$UserMetadataEntityTable.$converterkey.toJson(key)),
|
||||||
.toJson(preferences)),
|
'value': serializer.toJson<Object?>(
|
||||||
|
i1.$UserMetadataEntityTable.$convertervalue.toJson(value)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
i1.UserMetadataEntityData copyWith(
|
i1.UserMetadataEntityData copyWith(
|
||||||
{String? userId, i2.UserPreferences? preferences}) =>
|
{String? userId,
|
||||||
|
i2.UserMetadataKey? key,
|
||||||
|
Map<String, Object?>? value}) =>
|
||||||
i1.UserMetadataEntityData(
|
i1.UserMetadataEntityData(
|
||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
preferences: preferences ?? this.preferences,
|
key: key ?? this.key,
|
||||||
|
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,
|
||||||
preferences:
|
key: data.key.present ? data.key.value : this.key,
|
||||||
data.preferences.present ? data.preferences.value : this.preferences,
|
value: data.value.present ? data.value.value : this.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,49 +434,60 @@ class UserMetadataEntityData extends i0.DataClass
|
|||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('UserMetadataEntityData(')
|
return (StringBuffer('UserMetadataEntityData(')
|
||||||
..write('userId: $userId, ')
|
..write('userId: $userId, ')
|
||||||
..write('preferences: $preferences')
|
..write('key: $key, ')
|
||||||
|
..write('value: $value')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(userId, preferences);
|
int get hashCode => Object.hash(userId, key, value);
|
||||||
@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.preferences == this.preferences);
|
other.key == this.key &&
|
||||||
|
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.UserPreferences> preferences;
|
final i0.Value<i2.UserMetadataKey> key;
|
||||||
|
final i0.Value<Map<String, Object?>> value;
|
||||||
const UserMetadataEntityCompanion({
|
const UserMetadataEntityCompanion({
|
||||||
this.userId = const i0.Value.absent(),
|
this.userId = const i0.Value.absent(),
|
||||||
this.preferences = const i0.Value.absent(),
|
this.key = const i0.Value.absent(),
|
||||||
|
this.value = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
UserMetadataEntityCompanion.insert({
|
UserMetadataEntityCompanion.insert({
|
||||||
required String userId,
|
required String userId,
|
||||||
required i2.UserPreferences preferences,
|
required i2.UserMetadataKey key,
|
||||||
|
required Map<String, Object?> value,
|
||||||
}) : userId = i0.Value(userId),
|
}) : userId = i0.Value(userId),
|
||||||
preferences = i0.Value(preferences);
|
key = i0.Value(key),
|
||||||
|
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<String>? preferences,
|
i0.Expression<int>? key,
|
||||||
|
i0.Expression<i3.Uint8List>? value,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (userId != null) 'user_id': userId,
|
if (userId != null) 'user_id': userId,
|
||||||
if (preferences != null) 'preferences': preferences,
|
if (key != null) 'key': key,
|
||||||
|
if (value != null) 'value': value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
i1.UserMetadataEntityCompanion copyWith(
|
i1.UserMetadataEntityCompanion copyWith(
|
||||||
{i0.Value<String>? userId, i0.Value<i2.UserPreferences>? preferences}) {
|
{i0.Value<String>? userId,
|
||||||
|
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,
|
||||||
preferences: preferences ?? this.preferences,
|
key: key ?? this.key,
|
||||||
|
value: value ?? this.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,10 +497,13 @@ 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 (preferences.present) {
|
if (key.present) {
|
||||||
map['preferences'] = i0.Variable<String>(i1
|
map['key'] = i0.Variable<int>(
|
||||||
.$UserMetadataEntityTable.$converterpreferences
|
i1.$UserMetadataEntityTable.$converterkey.toSql(key.value));
|
||||||
.toSql(preferences.value));
|
}
|
||||||
|
if (value.present) {
|
||||||
|
map['value'] = i0.Variable<i3.Uint8List>(
|
||||||
|
i1.$UserMetadataEntityTable.$convertervalue.toSql(value.value));
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@@ -458,7 +512,8 @@ class UserMetadataEntityCompanion
|
|||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('UserMetadataEntityCompanion(')
|
return (StringBuffer('UserMetadataEntityCompanion(')
|
||||||
..write('userId: $userId, ')
|
..write('userId: $userId, ')
|
||||||
..write('preferences: $preferences')
|
..write('key: $key, ')
|
||||||
|
..write('value: $value')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_flutter/drift_flutter.dart';
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
@@ -17,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import 'db.repository.drift.dart';
|
import 'db.repository.drift.dart';
|
||||||
@@ -68,10 +70,36 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
onUpgrade: (m, from, to) async {
|
||||||
|
// Run migration steps without foreign keys and re-enable them later
|
||||||
|
await customStatement('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
|
await m.runMigrationSteps(
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
steps: migrationSteps(
|
||||||
|
from1To2: (m, _) async {
|
||||||
|
for (final entity in allSchemaEntities) {
|
||||||
|
await m.drop(entity);
|
||||||
|
await m.create(entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
// Fail if the migration broke foreign keys
|
||||||
|
final wrongFKs =
|
||||||
|
await customSelect('PRAGMA foreign_key_check').get();
|
||||||
|
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
|
||||||
|
}
|
||||||
|
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON;');
|
||||||
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
await customStatement('PRAGMA foreign_keys = ON');
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
await customStatement('PRAGMA synchronous = NORMAL');
|
await customStatement('PRAGMA synchronous = NORMAL');
|
||||||
|
|||||||
864
mobile/lib/infrastructure/repositories/db.repository.steps.dart
Normal file
864
mobile/lib/infrastructure/repositories/db.repository.steps.dart
Normal file
@@ -0,0 +1,864 @@
|
|||||||
|
// dart format width=80
|
||||||
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
|
import 'package:drift/drift.dart' as i1;
|
||||||
|
import 'dart:typed_data' as i2;
|
||||||
|
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||||
|
|
||||||
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
|
final class Schema2 extends i0.VersionedSchema {
|
||||||
|
Schema2({required super.database}) : super(version: 2);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
uQRemoteAssetOwnerChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
];
|
||||||
|
late final Shape0 userEntity = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape1 remoteAssetEntity = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape2 localAssetEntity = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_21,
|
||||||
|
_column_14,
|
||||||
|
_column_22,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index('idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
|
||||||
|
final i1.Index uQRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_asset_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)');
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
|
||||||
|
late final Shape3 userMetadataEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(user_id, "key")',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_23,
|
||||||
|
_column_24,
|
||||||
|
_column_25,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape4 partnerEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(shared_by_id, shared_with_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_26,
|
||||||
|
_column_27,
|
||||||
|
_column_28,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape5 localAlbumEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
_column_31,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape6 localAlbumAssetEntity = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id, album_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 remoteExifEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_34,
|
||||||
|
_column_35,
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape8 remoteAlbumEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_54,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_55,
|
||||||
|
_column_56,
|
||||||
|
_column_57,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape6 remoteAlbumAssetEntity = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id, album_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_34,
|
||||||
|
_column_58,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape9 remoteAlbumUserEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(album_id, user_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_58,
|
||||||
|
_column_23,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape10 memoryEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_60,
|
||||||
|
_column_61,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape11 memoryAssetEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(asset_id, memory_id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_34,
|
||||||
|
_column_66,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape12 stackEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape0 extends i0.VersionedTable {
|
||||||
|
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isAdmin =>
|
||||||
|
columnsByName['is_admin']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get email =>
|
||||||
|
columnsByName['email']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get profileImagePath =>
|
||||||
|
columnsByName['profile_image_path']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get quotaSizeInBytes =>
|
||||||
|
columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get quotaUsageInBytes =>
|
||||||
|
columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('name', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_2(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_admin', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_admin" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('email', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('profile_image_path', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'));
|
||||||
|
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('quota_size_in_bytes', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
class Shape1 extends i0.VersionedTable {
|
||||||
|
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||||
|
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get checksum =>
|
||||||
|
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get localDateTime =>
|
||||||
|
columnsByName['local_date_time']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get thumbHash =>
|
||||||
|
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get livePhotoVideoId =>
|
||||||
|
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get visibility =>
|
||||||
|
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_8(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('type', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'));
|
||||||
|
i1.GeneratedColumn<int> _column_10(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('width', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('height', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<int> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('duration_in_seconds', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('checksum', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_favorite', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_favorite" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('owner_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<DateTime> _column_16(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('local_date_time', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<String> _column_17(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('thumb_hash', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<String> _column_19(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('live_photo_video_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<int> _column_20(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('visibility', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
|
||||||
|
class Shape2 extends i0.VersionedTable {
|
||||||
|
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||||
|
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get checksum =>
|
||||||
|
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<int> get orientation =>
|
||||||
|
columnsByName['orientation']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_21(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('checksum', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<int> _column_22(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('orientation', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
class Shape3 extends i0.VersionedTable {
|
||||||
|
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get userId =>
|
||||||
|
columnsByName['user_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get key =>
|
||||||
|
columnsByName['key']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get value =>
|
||||||
|
columnsByName['value']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_23(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('user_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<int> _column_24(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('key', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> _column_25(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<i2.Uint8List>('value', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.blob);
|
||||||
|
|
||||||
|
class Shape4 extends i0.VersionedTable {
|
||||||
|
Shape4({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get sharedById =>
|
||||||
|
columnsByName['shared_by_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get sharedWithId =>
|
||||||
|
columnsByName['shared_with_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get inTimeline =>
|
||||||
|
columnsByName['in_timeline']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('shared_by_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('shared_with_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<bool> _column_28(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('in_timeline', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("in_timeline" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
class Shape5 extends i0.VersionedTable {
|
||||||
|
Shape5({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get backupSelection =>
|
||||||
|
columnsByName['backup_selection']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<bool> get isIosSharedAlbum =>
|
||||||
|
columnsByName['is_ios_shared_album']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<bool> get marker_ =>
|
||||||
|
columnsByName['marker']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('backup_selection', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_ios_shared_album', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_ios_shared_album" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<bool> _column_31(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('marker', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("marker" IN (0, 1))'));
|
||||||
|
|
||||||
|
class Shape6 extends i0.VersionedTable {
|
||||||
|
Shape6({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get albumId =>
|
||||||
|
columnsByName['album_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('asset_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES local_asset_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('album_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES local_album_entity (id) ON DELETE CASCADE'));
|
||||||
|
|
||||||
|
class Shape7 extends i0.VersionedTable {
|
||||||
|
Shape7({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get city =>
|
||||||
|
columnsByName['city']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get state =>
|
||||||
|
columnsByName['state']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get country =>
|
||||||
|
columnsByName['country']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get dateTimeOriginal =>
|
||||||
|
columnsByName['date_time_original']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get exposureTime =>
|
||||||
|
columnsByName['exposure_time']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<double> get fNumber =>
|
||||||
|
columnsByName['f_number']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<int> get fileSize =>
|
||||||
|
columnsByName['file_size']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<double> get focalLength =>
|
||||||
|
columnsByName['focal_length']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<double> get latitude =>
|
||||||
|
columnsByName['latitude']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<double> get longitude =>
|
||||||
|
columnsByName['longitude']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<int> get iso =>
|
||||||
|
columnsByName['iso']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get make =>
|
||||||
|
columnsByName['make']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get model =>
|
||||||
|
columnsByName['model']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get lens =>
|
||||||
|
columnsByName['lens']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get orientation =>
|
||||||
|
columnsByName['orientation']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get timeZone =>
|
||||||
|
columnsByName['time_zone']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get rating =>
|
||||||
|
columnsByName['rating']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get projectionType =>
|
||||||
|
columnsByName['projection_type']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('asset_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
|
||||||
|
i1.GeneratedColumn<String> _column_35(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('city', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('state', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('country', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_38(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('date_time_original', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('description', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('exposure_time', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<double> _column_41(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('f_number', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('file_size', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<double> _column_43(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('focal_length', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<double> _column_44(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('latitude', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<double> _column_45(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<double>('longitude', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.double);
|
||||||
|
i1.GeneratedColumn<int> _column_46(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('iso', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_47(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('make', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_48(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('model', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_49(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('lens', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_50(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('orientation', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_51(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('time_zone', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<int> _column_52(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('rating', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_53(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('projection_type', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
|
||||||
|
class Shape8 extends i0.VersionedTable {
|
||||||
|
Shape8({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get thumbnailAssetId =>
|
||||||
|
columnsByName['thumbnail_asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isActivityEnabled =>
|
||||||
|
columnsByName['is_activity_enabled']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<int> get order =>
|
||||||
|
columnsByName['order']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_54(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('description', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultValue: const CustomExpression('\'\''));
|
||||||
|
i1.GeneratedColumn<String> _column_55(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('thumbnail_asset_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
|
||||||
|
i1.GeneratedColumn<bool> _column_56(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_activity_enabled', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_activity_enabled" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('1'));
|
||||||
|
i1.GeneratedColumn<int> _column_57(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('order', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
i1.GeneratedColumn<String> _column_58(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('album_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_album_entity (id) ON DELETE CASCADE'));
|
||||||
|
|
||||||
|
class Shape9 extends i0.VersionedTable {
|
||||||
|
Shape9({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get albumId =>
|
||||||
|
columnsByName['album_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get userId =>
|
||||||
|
columnsByName['user_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get role =>
|
||||||
|
columnsByName['role']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_59(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('role', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
|
||||||
|
class Shape10 extends i0.VersionedTable {
|
||||||
|
Shape10({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get data =>
|
||||||
|
columnsByName['data']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isSaved =>
|
||||||
|
columnsByName['is_saved']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<DateTime> get memoryAt =>
|
||||||
|
columnsByName['memory_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get seenAt =>
|
||||||
|
columnsByName['seen_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get showAt =>
|
||||||
|
columnsByName['show_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get hideAt =>
|
||||||
|
columnsByName['hide_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_60(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('data', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_61(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('is_saved', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_saved" IN (0, 1))'),
|
||||||
|
defaultValue: const CustomExpression('0'));
|
||||||
|
i1.GeneratedColumn<DateTime> _column_62(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('memory_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_63(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('seen_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_64(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('show_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
i1.GeneratedColumn<DateTime> _column_65(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('hide_at', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
|
||||||
|
class Shape11 extends i0.VersionedTable {
|
||||||
|
Shape11({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get memoryId =>
|
||||||
|
columnsByName['memory_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_66(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('memory_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES memory_entity (id) ON DELETE CASCADE'));
|
||||||
|
|
||||||
|
class Shape12 extends i0.VersionedTable {
|
||||||
|
Shape12({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<String> get ownerId =>
|
||||||
|
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get primaryAssetId =>
|
||||||
|
columnsByName['primary_asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_67(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id)'));
|
||||||
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
}) {
|
||||||
|
return (currentVersion, database) async {
|
||||||
|
switch (currentVersion) {
|
||||||
|
case 1:
|
||||||
|
final schema = Schema2(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from1To2(migrator, schema);
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.OnUpgrade stepByStep({
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
}) =>
|
||||||
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
|
step: migrationSteps(
|
||||||
|
from1To2: from1To2,
|
||||||
|
));
|
||||||
@@ -281,6 +281,7 @@ 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),
|
||||||
);
|
);
|
||||||
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||||
@@ -361,6 +362,24 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
|
batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
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.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
@@ -11,7 +12,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
Stream<LocalAsset?> watchAsset(String id) {
|
Stream<LocalAsset?> watchAsset(String id) {
|
||||||
final query = _db.localAssetEntity
|
final query = _db.localAssetEntity
|
||||||
.select()
|
.select()
|
||||||
.addColumns([_db.localAssetEntity.id]).join([
|
.addColumns([_db.remoteAssetEntity.id]).join([
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
_db.remoteAssetEntity,
|
_db.remoteAssetEntity,
|
||||||
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
@@ -43,4 +44,16 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> delete(List<String> ids) {
|
||||||
|
if (ids.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _db.batch((batch) {
|
||||||
|
for (final slice in ids.slices(32000)) {
|
||||||
|
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
|
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 }
|
enum SortRemoteAlbumsBy { id, updatedAt }
|
||||||
|
|
||||||
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<Album>> getAll({Set<SortRemoteAlbumsBy> sortBy = const {}}) {
|
Future<List<RemoteAlbum>> getAll({
|
||||||
|
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([
|
||||||
@@ -18,13 +27,21 @@ 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) {
|
||||||
@@ -33,6 +50,8 @@ 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),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -43,16 +62,217 @@ 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.readTable(_db.userEntity).name,
|
ownerName: row.read(_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<int> removeAssets(String albumId, List<String> assetIds) {
|
||||||
|
return _db.remoteAlbumAssetEntity.deleteWhere(
|
||||||
|
(tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 {
|
||||||
Album toDto({int assetCount = 0, required String ownerName}) {
|
RemoteAlbum toDto({int assetCount = 0, required String ownerName}) {
|
||||||
return Album(
|
return RemoteAlbum(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
|
|||||||
@@ -56,6 +56,42 @@ 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) {
|
||||||
|
|||||||
@@ -1,29 +1,22 @@
|
|||||||
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(LocalAsset asset) async {
|
Future<File?> getFileForAsset(String assetId) async {
|
||||||
final log = Logger('StorageRepository');
|
final log = Logger('StorageRepository');
|
||||||
File? file;
|
File? file;
|
||||||
try {
|
try {
|
||||||
final entity = await AssetEntity.fromId(asset.id);
|
final entity = await AssetEntity.fromId(assetId);
|
||||||
file = await entity?.originFile;
|
file = await entity?.originFile;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
log.warning(
|
log.warning("Cannot get file for asset $assetId");
|
||||||
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
log.warning(
|
log.warning("Error getting file for asset $assetId", error, stackTrace);
|
||||||
"Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
|
||||||
error,
|
|
||||||
stackTrace,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.memoryToAssetsV1,
|
SyncRequestType.memoryToAssetsV1,
|
||||||
SyncRequestType.stacksV1,
|
SyncRequestType.stacksV1,
|
||||||
SyncRequestType.partnerStacksV1,
|
SyncRequestType.partnerStacksV1,
|
||||||
|
SyncRequestType.userMetadataV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
@@ -170,6 +171,8 @@ 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 {
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ 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;
|
||||||
@@ -134,6 +136,7 @@ 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(
|
||||||
@@ -460,6 +463,53 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
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 {
|
||||||
@@ -505,6 +555,15 @@ 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 {
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ 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';
|
||||||
|
|
||||||
@@ -30,19 +33,19 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.map((users) => users..add(userId));
|
.map((users) => users..add(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Bucket> _generateBuckets(int count) {
|
TimelineQuery main(List<String> userIds, GroupAssetsBy groupBy) => (
|
||||||
final numBuckets = (count / kTimelineNoneSegmentSize).floor();
|
bucketSource: () => _watchMainBucket(
|
||||||
final buckets = List.generate(
|
userIds,
|
||||||
numBuckets,
|
groupBy: groupBy,
|
||||||
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize),
|
),
|
||||||
);
|
assetSource: (offset, count) => _getMainBucketAssets(
|
||||||
if (count % kTimelineNoneSegmentSize != 0) {
|
userIds,
|
||||||
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize));
|
offset: offset,
|
||||||
}
|
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,
|
||||||
}) {
|
}) {
|
||||||
@@ -62,7 +65,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,
|
||||||
@@ -70,41 +73,54 @@ 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) => row.remoteId != null && row.ownerId != null
|
||||||
return row.remoteId != null && row.ownerId != null
|
? RemoteAsset(
|
||||||
? RemoteAsset(
|
id: row.remoteId!,
|
||||||
id: row.remoteId!,
|
localId: row.localId,
|
||||||
localId: row.localId,
|
name: row.name,
|
||||||
name: row.name,
|
ownerId: row.ownerId!,
|
||||||
ownerId: row.ownerId!,
|
checksum: row.checksum,
|
||||||
checksum: row.checksum,
|
type: row.type,
|
||||||
type: row.type,
|
createdAt: row.createdAt,
|
||||||
createdAt: row.createdAt,
|
updatedAt: row.updatedAt,
|
||||||
updatedAt: row.updatedAt,
|
thumbHash: row.thumbHash,
|
||||||
thumbHash: row.thumbHash,
|
width: row.width,
|
||||||
width: row.width,
|
height: row.height,
|
||||||
height: row.height,
|
isFavorite: row.isFavorite,
|
||||||
isFavorite: row.isFavorite,
|
durationInSeconds: row.durationInSeconds,
|
||||||
durationInSeconds: row.durationInSeconds,
|
livePhotoVideoId: row.livePhotoVideoId,
|
||||||
)
|
)
|
||||||
: 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchLocalAlbumBucket(
|
TimelineQuery localAlbum(String albumId, GroupAssetsBy groupBy) => (
|
||||||
|
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,
|
||||||
}) {
|
}) {
|
||||||
@@ -118,14 +134,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()
|
final query = _db.localAssetEntity.selectOnly().join([
|
||||||
|
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)]);
|
||||||
@@ -137,7 +153,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,
|
||||||
@@ -147,18 +163,32 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchRemoteAlbumBucket(
|
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
|
||||||
|
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,
|
||||||
}) {
|
}) {
|
||||||
@@ -166,7 +196,177 @@ 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)
|
||||||
.watchSingle();
|
.watch()
|
||||||
|
.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 {
|
||||||
|
return await transaction(() 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();
|
||||||
@@ -176,268 +376,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
..addColumns([assetCountExp, dateExp])
|
..addColumns([assetCountExp, dateExp])
|
||||||
..join([
|
..join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.remoteAlbumAssetEntity,
|
_db.remoteExifEntity,
|
||||||
_db.remoteAlbumAssetEntity.assetId
|
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
|
||||||
.equalsExp(_db.remoteAssetEntity.id),
|
useColumns: false,
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
..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.remoteAssetEntity.ownerId.equals(userId) &
|
_db.remoteExifEntity.city.equals(place) &
|
||||||
_db.remoteAssetEntity.isFavorite.equals(true),
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
)
|
|
||||||
..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),
|
||||||
)
|
)
|
||||||
@@ -451,39 +397,51 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
}).watch();
|
}).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> getVideoBucketAssets(
|
Future<List<BaseAsset>> _getPlaceBucketAssets(
|
||||||
String userId, {
|
String place, {
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
}) {
|
}) {
|
||||||
final query = _db.remoteAssetEntity.select()
|
final query = _db.remoteAssetEntity.select().join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteExifEntity,
|
||||||
|
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
..where(
|
..where(
|
||||||
(row) =>
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
|
|
||||||
_db.remoteAssetEntity.visibility
|
_db.remoteAssetEntity.visibility
|
||||||
.equalsValue(AssetVisibility.timeline) &
|
.equalsValue(AssetVisibility.timeline) &
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId),
|
_db.remoteExifEntity.city.equals(place),
|
||||||
)
|
)
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
return query
|
||||||
return query.map((row) => row.toDto()).get();
|
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Bucket>> watchRemoteBucket(
|
TimelineQuery _remoteQueryBuilder({
|
||||||
List<String> userIds, {
|
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
||||||
|
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) {
|
||||||
return _db.remoteAssetEntity
|
final query = _db.remoteAssetEntity.count(where: filter);
|
||||||
.count(
|
return query.map(_generateBuckets).watchSingle();
|
||||||
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();
|
||||||
@@ -491,12 +449,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
final query = _db.remoteAssetEntity.selectOnly()
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
..addColumns([assetCountExp, dateExp])
|
..addColumns([assetCountExp, dateExp])
|
||||||
..where(
|
..where(filter(_db.remoteAssetEntity))
|
||||||
_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)]);
|
||||||
|
|
||||||
@@ -507,25 +460,33 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
}).watch();
|
}).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BaseAsset>> getRemoteBucketAssets(
|
Future<List<BaseAsset>> _getRemoteAssets({
|
||||||
List<String> userIds, {
|
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
|
||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
}) {
|
}) async {
|
||||||
final query = _db.remoteAssetEntity.select()
|
return await transaction(() async {
|
||||||
..where(
|
final query = _db.remoteAssetEntity.select()
|
||||||
(row) =>
|
..where(filter)
|
||||||
row.deletedAt.isNull() &
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
row.visibility.equalsValue(AssetVisibility.timeline) &
|
..limit(count, offset: offset);
|
||||||
row.ownerId.isIn(userIds),
|
|
||||||
)
|
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
|
||||||
..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
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -23,14 +23,15 @@ 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';
|
||||||
@@ -98,6 +99,17 @@ 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 {
|
||||||
@@ -176,10 +188,13 @@ 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,
|
||||||
currentRouteName == SplashScreenRoute.name,
|
isColdStart,
|
||||||
);
|
);
|
||||||
|
|
||||||
return proposedRoute;
|
return proposedRoute;
|
||||||
@@ -188,7 +203,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,
|
||||||
currentRouteName == SplashScreenRoute.name,
|
isColdStart,
|
||||||
);
|
);
|
||||||
|
|
||||||
return proposedRoute;
|
return proposedRoute;
|
||||||
@@ -250,7 +265,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
),
|
),
|
||||||
routerConfig: router.config(
|
routerConfig: router.config(
|
||||||
deepLinkBuilder: _deepLinkBuilder,
|
deepLinkBuilder: _deepLinkBuilder,
|
||||||
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
navigatorObservers: () =>
|
||||||
|
[AppNavigationObserver(ref: ref), HeroController()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,52 +1,40 @@
|
|||||||
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 {
|
||||||
void Function() onAddPhotosPressed;
|
final void Function()? onAddPhotosPressed;
|
||||||
void Function() onAddUsersPressed;
|
final void Function()? onAddUsersPressed;
|
||||||
|
|
||||||
AlbumControlButton({
|
const AlbumControlButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onAddPhotosPressed,
|
this.onAddPhotosPressed,
|
||||||
required this.onAddUsersPressed,
|
this.onAddUsersPressed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final userId = ref.watch(authProvider).userId;
|
return SizedBox(
|
||||||
final isOwner = ref.watch(
|
height: 36,
|
||||||
currentAlbumProvider.select((album) {
|
child: ListView(
|
||||||
return album?.ownerId == userId;
|
scrollDirection: Axis.horizontal,
|
||||||
}),
|
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 (isOwner)
|
if (onAddUsersPressed != null)
|
||||||
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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ 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 =
|
||||||
@@ -138,10 +143,13 @@ class AlbumViewer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const AlbumSharedUserIcons(),
|
const AlbumSharedUserIcons(),
|
||||||
if (album.isRemote)
|
if (album.isRemote)
|
||||||
AlbumControlButton(
|
Padding(
|
||||||
key: const ValueKey("albumControlButton"),
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
onAddPhotosPressed: onAddPhotosPressed,
|
child: AlbumControlButton(
|
||||||
onAddUsersPressed: onAddUsersPressed,
|
key: const ValueKey("albumControlButton"),
|
||||||
|
onAddPhotosPressed: onAddPhotosPressed,
|
||||||
|
onAddUsersPressed: isOwner ? onAddUsersPressed : null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
|
|||||||
126
mobile/lib/pages/common/change_experience.page.dart
Normal file
126
mobile/lib/pages/common/change_experience.page.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class ChangeExperiencePage extends ConsumerStatefulWidget {
|
||||||
|
final bool switchingToBeta;
|
||||||
|
|
||||||
|
const ChangeExperiencePage({super.key, required this.switchingToBeta});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _ChangeExperiencePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
|
bool hasMigrated = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleMigration() async {
|
||||||
|
if (widget.switchingToBeta) {
|
||||||
|
final assetNotifier = ref.read(assetProvider.notifier);
|
||||||
|
if (assetNotifier.mounted) {
|
||||||
|
assetNotifier.dispose();
|
||||||
|
}
|
||||||
|
final albumNotifier = ref.read(albumProvider.notifier);
|
||||||
|
if (albumNotifier.mounted) {
|
||||||
|
albumNotifier.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
final permission = await ref
|
||||||
|
.read(galleryPermissionNotifier.notifier)
|
||||||
|
.requestGalleryPermission();
|
||||||
|
|
||||||
|
if (permission.isGranted) {
|
||||||
|
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
|
await migrateDeviceAssetToSqlite(
|
||||||
|
ref.read(isarProvider),
|
||||||
|
ref.read(driftProvider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ref.read(backgroundSyncProvider).cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
context.replaceRoute(
|
||||||
|
widget.switchingToBeta
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
hasMigrated = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Durations.long4,
|
||||||
|
child: hasMigrated
|
||||||
|
? const Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 48.0,
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
width: 50.0,
|
||||||
|
height: 50.0,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 300.0,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: Durations.long4,
|
||||||
|
child: hasMigrated
|
||||||
|
? Text(
|
||||||
|
"Migration success. Navigating to the new timeline...",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
"Data migration in progress...\nPlease wait and don't close this page",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
|||||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||||
@@ -94,55 +95,59 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
const _MobileLayout();
|
const _MobileLayout();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final List<Widget> settings = SettingSection.values
|
||||||
|
.map(
|
||||||
|
(setting) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
leading: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
color: context.isDarkTheme
|
||||||
|
? Colors.black26
|
||||||
|
: Colors.white.withAlpha(100),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Icon(setting.icon, color: context.primaryColor),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
setting.title,
|
||||||
|
style: context.textTheme.titleMedium!.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
setting.subtitle,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
).tr(),
|
||||||
|
onTap: () =>
|
||||||
|
context.pushRoute(SettingsSubRoute(section: setting)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
return ListView(
|
return ListView(
|
||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||||
children: SettingSection.values
|
children: [
|
||||||
.map(
|
const BetaTimelineListTile(),
|
||||||
(setting) => Padding(
|
...settings,
|
||||||
padding: const EdgeInsets.symmetric(
|
],
|
||||||
horizontal: 16.0,
|
|
||||||
),
|
|
||||||
child: Card(
|
|
||||||
elevation: 0,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
color: context.colorScheme.surfaceContainer,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
),
|
|
||||||
leading: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
color: context.isDarkTheme
|
|
||||||
? Colors.black26
|
|
||||||
: Colors.white.withAlpha(100),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Icon(setting.icon, color: context.primaryColor),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
setting.title,
|
|
||||||
style: context.textTheme.titleMedium!.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
subtitle: Text(
|
|
||||||
setting.subtitle,
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
).tr(),
|
|
||||||
onTap: () =>
|
|
||||||
context.pushRoute(SettingsSubRoute(section: setting)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,15 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.router.current.name == SplashScreenRoute.name) {
|
if (context.router.current.name == SplashScreenRoute.name) {
|
||||||
context.replaceRoute(const TabControllerRoute());
|
context.replaceRoute(
|
||||||
|
Store.isBetaTimelineEnabled
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Store.isBetaTimelineEnabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasPermission =
|
final hasPermission =
|
||||||
|
|||||||
@@ -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/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';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class TabControllerPage extends HookConsumerWidget {
|
class TabControllerPage extends HookConsumerWidget {
|
||||||
@@ -158,10 +158,6 @@ 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, _) =>
|
||||||
@@ -173,10 +169,10 @@ class TabControllerPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
navigationRail(tabsRouter),
|
navigationRail(tabsRouter),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(child: heroedChild),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: heroedChild,
|
: child,
|
||||||
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
||||||
? null
|
? null
|
||||||
: bottomNavigationBar(tabsRouter),
|
: bottomNavigationBar(tabsRouter),
|
||||||
|
|||||||
@@ -5,17 +5,33 @@ 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';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class TabShellPage extends ConsumerWidget {
|
class TabShellPage extends ConsumerStatefulWidget {
|
||||||
const TabShellPage({super.key});
|
const TabShellPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<TabShellPage> createState() => _TabShellPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
runNewSync(ref, full: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
|
||||||
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||||
@@ -127,10 +143,6 @@ 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, _) =>
|
||||||
@@ -142,10 +154,10 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
navigationRail(tabsRouter),
|
navigationRail(tabsRouter),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(child: heroedChild),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: heroedChild,
|
: child,
|
||||||
bottomNavigationBar: _BottomNavigationBar(
|
bottomNavigationBar: _BottomNavigationBar(
|
||||||
tabsRouter: tabsRouter,
|
tabsRouter: tabsRouter,
|
||||||
destinations: navigationDestinations,
|
destinations: navigationDestinations,
|
||||||
@@ -168,6 +180,11 @@ 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];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
||||||
@@ -75,7 +76,9 @@ class ShareIntentPage extends HookConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.navigateTo(
|
context.navigateTo(
|
||||||
const TabControllerRoute(),
|
Store.isBetaTimelineEnabled
|
||||||
|
? const TabShellRoute()
|
||||||
|
: const TabControllerRoute(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
|||||||
5
mobile/lib/platform/native_sync_api.g.dart
generated
5
mobile/lib/platform/native_sync_api.g.dart
generated
@@ -40,6 +40,7 @@ class PlatformAsset {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
required this.durationInSeconds,
|
required this.durationInSeconds,
|
||||||
|
required this.orientation,
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
@@ -58,6 +59,8 @@ class PlatformAsset {
|
|||||||
|
|
||||||
int durationInSeconds;
|
int durationInSeconds;
|
||||||
|
|
||||||
|
int orientation;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
id,
|
id,
|
||||||
@@ -68,6 +71,7 @@ class PlatformAsset {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
|
orientation,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +90,7 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user