Compare commits
69 Commits
dev/better
...
feat/ml-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
683bb88f8b | ||
|
|
ae80def7f2 | ||
|
|
069a32dcdb | ||
|
|
388144823a | ||
|
|
c23d84be39 | ||
|
|
66120025b7 | ||
|
|
da33653b0a | ||
|
|
3ea0210c1d | ||
|
|
98f1e85c87 | ||
|
|
d2509c619e | ||
|
|
2bfe5d1573 | ||
|
|
d7d464570f | ||
|
|
2e82476cff | ||
|
|
2f462717aa | ||
|
|
86e04832a1 | ||
|
|
96f1a271ef | ||
|
|
55e3605ca4 | ||
|
|
0bf55d8e32 | ||
|
|
2dcad93d9c | ||
|
|
328a58ac0d | ||
|
|
7fca0d8da5 | ||
|
|
413ab2c538 | ||
|
|
394e0dfe37 | ||
|
|
a9b6acec28 | ||
|
|
ad4cbf20de | ||
|
|
26fd797ac9 | ||
|
|
35767591d2 | ||
|
|
3b11854702 | ||
|
|
895129c997 | ||
|
|
92ec1ce77f | ||
|
|
986bbfa831 | ||
|
|
75c065c83a | ||
|
|
9c0805c37a | ||
|
|
bffc2cdf60 | ||
|
|
a147dee4b6 | ||
|
|
5423f1c25b | ||
|
|
5c602bf4d4 | ||
|
|
5db73c5c5c | ||
|
|
52fe392a9e | ||
|
|
5e1c0fb465 | ||
|
|
37ab37bffc | ||
|
|
664b7106ca | ||
|
|
bb28cae671 | ||
|
|
c2c26c471a | ||
|
|
2dca2850dc | ||
|
|
7fc8f6433b | ||
|
|
f6180fccdc | ||
|
|
9d01885b58 | ||
|
|
ace0a5911c | ||
|
|
21f2d3058a | ||
|
|
26fd9d7e5f | ||
|
|
c74ea7282a | ||
|
|
279481ad54 | ||
|
|
9e7a32804b | ||
|
|
a0743d8b7d | ||
|
|
68000c21a8 | ||
|
|
e671b30aaf | ||
|
|
cf1dfdc776 | ||
|
|
de29480dda | ||
|
|
2e424fe249 | ||
|
|
d4ef6f52bb | ||
|
|
e1e45f3f32 | ||
|
|
330f4cadda | ||
|
|
621eef0edc | ||
|
|
33ce2b7bba | ||
|
|
81792a5342 | ||
|
|
5f43971ccf | ||
|
|
38443a6068 | ||
|
|
92bb42950e |
4
.github/workflows/docker-cleanup.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Clean temporary images
|
name: Clean temporary images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
|
uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "immich-app"
|
owner: "immich-app"
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Clean untagged images
|
name: Clean untagged images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/untagged@v0.3.0
|
uses: stumpylog/image-cleaner-action/untagged@v0.4.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "immich-app"
|
owner: "immich-app"
|
||||||
|
|||||||
83
.github/workflows/docker.yml
vendored
@@ -33,91 +33,10 @@ jobs:
|
|||||||
- context: "nginx"
|
- context: "nginx"
|
||||||
image: "immich-proxy"
|
image: "immich-proxy"
|
||||||
platforms: "linux/amd64,linux/arm64"
|
platforms: "linux/amd64,linux/arm64"
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.0.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
|
||||||
# Workaround to fix error:
|
|
||||||
# failed to push: failed to copy: io: read/write on closed pipe
|
|
||||||
# See https://github.com/docker/build-push-action/issues/761
|
|
||||||
with:
|
|
||||||
driver-opts: |
|
|
||||||
image=moby/buildkit:v0.10.6
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
# Only push to Docker Hub when making a release
|
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
# Skip when PR from a fork
|
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate docker image tags
|
|
||||||
id: metadata
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
flavor: |
|
|
||||||
# Disable latest tag
|
|
||||||
latest=false
|
|
||||||
images: |
|
|
||||||
name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
|
|
||||||
name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
|
|
||||||
tags: |
|
|
||||||
# Tag with branch name
|
|
||||||
type=ref,event=branch
|
|
||||||
# Tag with pr-number
|
|
||||||
type=ref,event=pr
|
|
||||||
# Tag with git tag on release
|
|
||||||
type=ref,event=tag
|
|
||||||
type=raw,value=release,enable=${{ github.event_name == 'release' }}
|
|
||||||
|
|
||||||
- name: Determine build cache output
|
|
||||||
id: cache-target
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
||||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
|
||||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and push image
|
|
||||||
uses: docker/build-push-action@v5.0.0
|
|
||||||
with:
|
|
||||||
context: ${{ matrix.context }}
|
|
||||||
platforms: ${{ matrix.platforms }}
|
|
||||||
# Skip pushing when PR from a fork
|
|
||||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
|
||||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
|
||||||
|
|
||||||
build_and_push_server_arm_64:
|
|
||||||
runs-on: self-hosted
|
|
||||||
strategy:
|
|
||||||
# Prevent a failure in one image from stopping the other builds
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- context: "server"
|
- context: "server"
|
||||||
image: "immich-server"
|
image: "immich-server"
|
||||||
platforms: "linux/arm64,linux/amd64"
|
platforms: "linux/arm64,linux/amd64"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
18
Makefile
@@ -1,35 +1,29 @@
|
|||||||
dev:
|
dev:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
|
||||||
|
|
||||||
dev-new:
|
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
|
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
|
||||||
|
|
||||||
dev-down:
|
dev-down:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||||
|
|
||||||
dev-new-update:
|
dev-update:
|
||||||
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
stage:
|
stage:
|
||||||
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
pull-stage:
|
pull-stage:
|
||||||
docker-compose -f ./docker/docker-compose.staging.yml pull
|
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker-compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
api:
|
api:
|
||||||
cd ./server && npm run api:generate
|
cd ./server && npm run api:generate
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
|
||||||
<a href="https://discord.gg/D8JsnBEuKb">
|
<a href="https://discord.gg/D8JsnBEuKb">
|
||||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|||||||
130
cli/package-lock.json
generated
@@ -1489,21 +1489,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/byte-size": {
|
"node_modules/@types/byte-size": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz",
|
||||||
"integrity": "sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==",
|
"integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
|
||||||
"integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==",
|
"integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/cli-progress": {
|
"node_modules/@types/cli-progress": {
|
||||||
"version": "3.11.3",
|
"version": "3.11.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz",
|
||||||
"integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==",
|
"integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -1543,9 +1543,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/jest": {
|
"node_modules/@types/jest": {
|
||||||
"version": "29.5.5",
|
"version": "29.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
|
||||||
"integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
|
"integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expect": "^29.0.0",
|
"expect": "^29.0.0",
|
||||||
@@ -1553,9 +1553,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/js-yaml": {
|
"node_modules/@types/js-yaml": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
"integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==",
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
@@ -1565,25 +1565,28 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/mime-types": {
|
"node_modules/@types/mime-types": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
||||||
"integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==",
|
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/mock-fs": {
|
"node_modules/@types/mock-fs": {
|
||||||
"version": "4.13.2",
|
"version": "4.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
|
||||||
"integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==",
|
"integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.8.2",
|
"version": "20.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
|
||||||
"integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==",
|
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/normalize-package-data": {
|
"node_modules/@types/normalize-package-data": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
@@ -3867,9 +3870,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jest-extended": {
|
"node_modules/jest-extended": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz",
|
||||||
"integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==",
|
"integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jest-diff": "^29.0.0",
|
"jest-diff": "^29.0.0",
|
||||||
@@ -5862,6 +5865,12 @@
|
|||||||
"node": ">=4.2.0"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||||
@@ -7270,21 +7279,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/byte-size": {
|
"@types/byte-size": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz",
|
||||||
"integrity": "sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==",
|
"integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/chai": {
|
"@types/chai": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
|
||||||
"integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==",
|
"integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/cli-progress": {
|
"@types/cli-progress": {
|
||||||
"version": "3.11.3",
|
"version": "3.11.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz",
|
||||||
"integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==",
|
"integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -7324,9 +7333,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jest": {
|
"@types/jest": {
|
||||||
"version": "29.5.5",
|
"version": "29.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
|
||||||
"integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
|
"integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"expect": "^29.0.0",
|
"expect": "^29.0.0",
|
||||||
@@ -7334,9 +7343,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/js-yaml": {
|
"@types/js-yaml": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
"integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==",
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
@@ -7346,25 +7355,28 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/mime-types": {
|
"@types/mime-types": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
||||||
"integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==",
|
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/mock-fs": {
|
"@types/mock-fs": {
|
||||||
"version": "4.13.2",
|
"version": "4.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
|
||||||
"integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==",
|
"integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "20.8.2",
|
"version": "20.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
|
||||||
"integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==",
|
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@types/normalize-package-data": {
|
"@types/normalize-package-data": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
@@ -8969,9 +8981,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jest-extended": {
|
"jest-extended": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz",
|
||||||
"integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==",
|
"integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"jest-diff": "^29.0.0",
|
"jest-diff": "^29.0.0",
|
||||||
@@ -10419,6 +10431,12 @@
|
|||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"update-browserslist-db": {
|
"update-browserslist-db": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||||
|
|||||||
1773
cli/src/api/open-api/api.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.84.0
|
* The version of the OpenAPI document: 1.85.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
2
cli/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.84.0
|
* The version of the OpenAPI document: 1.85.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
2
cli/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.84.0
|
* The version of the OpenAPI document: 1.85.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
2
cli/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.84.0
|
* The version of the OpenAPI document: 1.85.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
# See:
|
||||||
|
# - https://immich.app/docs/developer/setup
|
||||||
|
# - https://immich.app/docs/developer/troubleshooting
|
||||||
|
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
|
name: immich-dev
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
@@ -71,10 +77,6 @@ services:
|
|||||||
command: npm run dev --host
|
command: npm run dev --host
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
|
||||||
# Rename these values for svelte public interface
|
|
||||||
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
|
|
||||||
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
|
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
@@ -121,11 +123,11 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
|
name: immich-prod
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
@@ -7,9 +9,9 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: ["./start-server.sh"]
|
command: [ "./start-server.sh" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -18,19 +20,6 @@ services:
|
|||||||
- database
|
- database
|
||||||
- typesense
|
- typesense
|
||||||
|
|
||||||
immich-machine-learning:
|
|
||||||
container_name: immich_machine_learning
|
|
||||||
image: immich-machine-learning:latest
|
|
||||||
build:
|
|
||||||
context: ../machine-learning
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
volumes:
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
- model-cache:/cache
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
image: immich-microservices:latest
|
image: immich-microservices:latest
|
||||||
@@ -40,9 +29,9 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: ["./start-microservices.sh"]
|
command: [ "./start-microservices.sh" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -64,6 +53,18 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
container_name: immich_machine_learning
|
||||||
|
image: immich-machine-learning:latest
|
||||||
|
build:
|
||||||
|
context: ../machine-learning
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- model-cache:/cache
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: always
|
||||||
|
|
||||||
typesense:
|
typesense:
|
||||||
container_name: immich_typesense
|
container_name: immich_typesense
|
||||||
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||||
@@ -73,17 +74,17 @@ services:
|
|||||||
# remove this to get debug messages
|
# remove this to get debug messages
|
||||||
- GLOG_minloglevel=1
|
- GLOG_minloglevel=1
|
||||||
volumes:
|
volumes:
|
||||||
- tsdata:/data
|
- ${UPLOAD_LOCATION}/typesense:/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -91,7 +92,7 @@ services:
|
|||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-proxy:
|
immich-proxy:
|
||||||
@@ -113,6 +114,4 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
|
||||||
model-cache:
|
model-cache:
|
||||||
tsdata:
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
- database
|
- database
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
|
||||||
command: -c fsync=off
|
command: -c fsync=off
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
|
name: immich
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
command: ["start.sh", "immich"]
|
command: [ "start.sh", "immich" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -22,7 +24,7 @@ services:
|
|||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.yml
|
# file: hwaccel.yml
|
||||||
# service: hwaccel
|
# service: hwaccel
|
||||||
command: ["start.sh", "microservices"]
|
command: [ "start.sh", "microservices" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@@ -64,12 +66,12 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/back
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash title='Restore'
|
```bash title='Restore'
|
||||||
docker-compose down -v # CAUTION! Deletes all Immich data to start from scratch.
|
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch.
|
||||||
docker-compose pull # Update to latest version of Immich (if desired)
|
docker compose pull # Update to latest version of Immich (if desired)
|
||||||
docker-compose create # Create Docker containers for Immich apps without running them.
|
docker compose create # Create Docker containers for Immich apps without running them.
|
||||||
docker start immich_postgres # Start Postgres server
|
docker start immich_postgres # Start Postgres server
|
||||||
sleep 10 # Wait for Postgres server to start up
|
sleep 10 # Wait for Postgres server to start up
|
||||||
gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql -U postgres -d immich # Restore Backup
|
gunzip < "/path/to/backup/dump.sql.gz" | docker exec -i immich_postgres psql -U postgres -d immich # Restore Backup
|
||||||
docker-compose up -d # Start remainder of Immich apps
|
docker compose up -d # Start remainder of Immich apps
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.).
|
Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.).
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ immich-admin list-users
|
|||||||
{
|
{
|
||||||
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
|
||||||
email: 'immich@example.com.com',
|
email: 'immich@example.com.com',
|
||||||
firstName: 'Immich',
|
name: 'Immich Admin',
|
||||||
lastName: 'Admin',
|
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
externalPath: null,
|
||||||
profileImagePath: 'upload/profile/e65e6f88-2a30-4dbe-8dd9-1885f4889b53/e65e6f88-2a30-4dbe-8dd9-1885f4889b53.jpg',
|
profileImagePath: 'upload/profile/e65e6f88-2a30-4dbe-8dd9-1885f4889b53/e65e6f88-2a30-4dbe-8dd9-1885f4889b53.jpg',
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ npm run typeorm:migrations:generate ./src/infra/<migration-name>
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Check if the migration file makes sense.
|
2. Check if the migration file makes sense.
|
||||||
3. Move the migration file to folder `./src/infra/database/migrations` in your code editor.
|
3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
|
||||||
|
|
||||||
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
||||||
|
|||||||
19
docs/docs/developer/troubleshooting.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.gg/D8JsnBEuKb) server, where we have a dedicated channel for `#contributing`.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
### Running on Windows
|
||||||
|
|
||||||
|
Running Immich on Windows can be frustrating and there are lots of ways it can go wrong. Where possible we recommend using Docker on Linux. However, several people have had success running Immich on Windows using Docker via WSL2.
|
||||||
|
|
||||||
|
### NTFS Mounted Volumes
|
||||||
|
|
||||||
|
The docker-compose.dev.yml and docker-compose.prod.yml use volume mounts for the postgres database. On start-up, postgres will try to `chown` the data directory, but fail. See [this post](https://forums.docker.com/t/data-directory-var-lib-postgresql-data-pgdata-has-wrong-ownership/17963/24) for more information about this issue and possible solutions.
|
||||||
|
|
||||||
|
### `Cannot read properties of null (reading 'split')`
|
||||||
|
|
||||||
|
This error occurs when trying to access the app via port `3000` instead of `2283`. During development `immich-proxy` runs on port 2283, while `immich-web` runs on `3000`.
|
||||||
@@ -4,6 +4,10 @@ You can use the CLI to upload an existing gallery to the Immich server
|
|||||||
|
|
||||||
[Immich CLI Repository](https://github.com/immich-app/CLI)
|
[Immich CLI Repository](https://github.com/immich-app/CLI)
|
||||||
|
|
||||||
|
:::tip Google Photos Takeout
|
||||||
|
If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
|
||||||
|
:::
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js 16 or above
|
- Node.js 16 or above
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Facial Recognition
|
# Facial Recognition
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
|
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
|
||||||
|
|
||||||
The list of people is shown in the Explore page.
|
The list of people is shown in the Explore page.
|
||||||
@@ -13,3 +15,16 @@ Upon clicking on a person, a list of assets that contain their face will be show
|
|||||||
The asset detail view will also show the faces that are recognized in the asset.
|
The asset detail view will also show the faces that are recognized in the asset.
|
||||||
|
|
||||||
<img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' />
|
<img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' />
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
Additional actions you can do with a detected person are:
|
||||||
|
|
||||||
|
- Change the feature face photo of the person
|
||||||
|
- Set date of birth
|
||||||
|
- Merge two or more detected faces into one person
|
||||||
|
- Hide face
|
||||||
|
|
||||||
|
It can be found from the app bar when you access the detial view of a person
|
||||||
|
|
||||||
|
<img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/>
|
||||||
|
|||||||
BIN
docs/docs/features/img/facial-recognition-4.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
@@ -75,7 +75,7 @@ Some basic examples:
|
|||||||
- `*.tif` will exclude all files with the extension `.tif`
|
- `*.tif` will exclude all files with the extension `.tif`
|
||||||
- `hidden.jpg` will exclude all files named `hidden.jpg`
|
- `hidden.jpg` will exclude all files named `hidden.jpg`
|
||||||
- `**/Raw/**` will exclude all files in any directory named `Raw`
|
- `**/Raw/**` will exclude all files in any directory named `Raw`
|
||||||
- `*.(tif,jpg)` will exclude all files with the extension `.tif` or `.jpg`
|
- `*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
|
||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import MobileAppDownload from '../partials/_mobile-app-download.md';
|
import MobileAppDownload from '../partials/_mobile-app-download.md';
|
||||||
import MobileAppLogin from '../partials/_mobile-app-login.md';
|
import MobileAppLogin from '../partials/_mobile-app-login.md';
|
||||||
import MobileAppBackup from '../partials/_mobile-app-login.md';
|
import MobileAppBackup from '../partials/_mobile-app-backup.md';
|
||||||
|
|
||||||
# Mobile App
|
# Mobile App
|
||||||
|
|
||||||
|
|||||||
42
docs/docs/guides/python-file-upload.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Python File Upload
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
API_KEY = 'YOUR_API_KEY' # replace with a valid api key
|
||||||
|
BASE_URL = 'http://127.0.0.1:2283/api' # replace as needed
|
||||||
|
|
||||||
|
|
||||||
|
def upload(file):
|
||||||
|
stats = os.stat(file)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'x-api-key': API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'deviceAssetId': f'{file}-{stats.st_mtime}',
|
||||||
|
'deviceId': 'python',
|
||||||
|
'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime),
|
||||||
|
'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime),
|
||||||
|
'isFavorite': 'false',
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'assetData': open(file, 'rb')
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f'{BASE_URL}/asset/upload', headers=headers, data=data, files=files)
|
||||||
|
|
||||||
|
print(response.json())
|
||||||
|
# {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False}
|
||||||
|
|
||||||
|
|
||||||
|
upload('./test.jpg')
|
||||||
|
```
|
||||||
@@ -17,6 +17,12 @@ The default configuration looks like this:
|
|||||||
"targetAudioCodec": "aac",
|
"targetAudioCodec": "aac",
|
||||||
"targetResolution": "720",
|
"targetResolution": "720",
|
||||||
"maxBitrate": "0",
|
"maxBitrate": "0",
|
||||||
|
"bframes": -1,
|
||||||
|
"refs": 0,
|
||||||
|
"gopSize": 0,
|
||||||
|
"npl": 0,
|
||||||
|
"temporalAQ": false,
|
||||||
|
"cqMode": "auto",
|
||||||
"twoPass": false,
|
"twoPass": false,
|
||||||
"transcode": "required",
|
"transcode": "required",
|
||||||
"tonemap": "hable",
|
"tonemap": "hable",
|
||||||
@@ -44,9 +50,15 @@ The default configuration looks like this:
|
|||||||
"sidecar": {
|
"sidecar": {
|
||||||
"concurrency": 5
|
"concurrency": 5
|
||||||
},
|
},
|
||||||
|
"library": {
|
||||||
|
"concurrency": 5
|
||||||
|
},
|
||||||
"storageTemplateMigration": {
|
"storageTemplateMigration": {
|
||||||
"concurrency": 5
|
"concurrency": 5
|
||||||
},
|
},
|
||||||
|
"migration": {
|
||||||
|
"concurrency": 5
|
||||||
|
},
|
||||||
"thumbnailGeneration": {
|
"thumbnailGeneration": {
|
||||||
"concurrency": 5
|
"concurrency": 5
|
||||||
},
|
},
|
||||||
@@ -55,16 +67,16 @@ The default configuration looks like this:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"machineLearning": {
|
"machineLearning": {
|
||||||
"classification": {
|
|
||||||
"minScore": 0.7,
|
|
||||||
"enabled": true,
|
|
||||||
"modelName": "microsoft/resnet-50"
|
|
||||||
},
|
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "http://immich-machine-learning:3003",
|
"url": "http://immich-machine-learning:3003",
|
||||||
|
"classification": {
|
||||||
|
"enabled": true,
|
||||||
|
"modelName": "microsoft/resnet-50",
|
||||||
|
"minScore": 0.9
|
||||||
|
},
|
||||||
"clip": {
|
"clip": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"modelName": "ViT-B-32::openai"
|
"modelName": "ViT-B-32__openai"
|
||||||
},
|
},
|
||||||
"facialRecognition": {
|
"facialRecognition": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -74,6 +86,14 @@ The default configuration looks like this:
|
|||||||
"minFaces": 1
|
"minFaces": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"map": {
|
||||||
|
"enabled": true,
|
||||||
|
"tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
},
|
||||||
|
"reverseGeocoding": {
|
||||||
|
"enabled": true,
|
||||||
|
"citiesFileOverride": "cities500"
|
||||||
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"issuerUrl": "",
|
"issuerUrl": "",
|
||||||
@@ -96,8 +116,27 @@ The default configuration looks like this:
|
|||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"webpSize": 250,
|
"webpSize": 250,
|
||||||
"jpegSize": 1440,
|
"jpegSize": 1440,
|
||||||
"quality": 90,
|
"quality": 80,
|
||||||
"colorspace": "p3"
|
"colorspace": "p3"
|
||||||
|
},
|
||||||
|
"newVersionCheck": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"trash": {
|
||||||
|
"enabled": true,
|
||||||
|
"days": 30
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"customCss": ""
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"scan": {
|
||||||
|
"enabled": true,
|
||||||
|
"cronExpression": "0 0 * * *"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stylesheets": {
|
||||||
|
"css": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 334 KiB |
20
docs/package-lock.json
generated
@@ -13232,19 +13232,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"fast-glob": "^3.2.12",
|
"fast-glob": "^3.3.0",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jiti": "^1.18.2",
|
"jiti": "^1.19.1",
|
||||||
"lilconfig": "^2.1.0",
|
"lilconfig": "^2.1.0",
|
||||||
"micromatch": "^4.0.5",
|
"micromatch": "^4.0.5",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -24517,19 +24517,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tailwindcss": {
|
"tailwindcss": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"fast-glob": "^3.2.12",
|
"fast-glob": "^3.3.0",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jiti": "^1.18.2",
|
"jiti": "^1.19.1",
|
||||||
"lilconfig": "^2.1.0",
|
"lilconfig": "^2.1.0",
|
||||||
"micromatch": "^4.0.5",
|
"micromatch": "^4.0.5",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function HomepageHeader() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src="/img/immich-screenshots.png" alt="logo" />
|
<img src="/img/immich-screenshots.png" alt="screenshots" width={'85%'} />
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
|
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
|
||||||
<div className="h-24">
|
<div className="h-24">
|
||||||
|
|||||||
BIN
docs/static/img/immich-screenshots.png
vendored
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.8 MiB |
@@ -38,8 +38,16 @@ class LogSettings(BaseSettings):
|
|||||||
_clean_name = str.maketrans(":\\/", "___", ".")
|
_clean_name = str.maketrans(":\\/", "___", ".")
|
||||||
|
|
||||||
|
|
||||||
|
def clean_name(model_name: str) -> str:
|
||||||
|
return model_name.split("/")[-1].translate(_clean_name)
|
||||||
|
|
||||||
|
|
||||||
def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
|
def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
|
||||||
return Path(settings.cache_folder) / model_type.value / model_name.translate(_clean_name)
|
return Path(settings.cache_folder) / model_type.value / clean_name(model_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_hf_model_name(model_name: str) -> str:
|
||||||
|
return f"immich-app/{clean_name(model_name)}"
|
||||||
|
|
||||||
|
|
||||||
LOG_LEVELS: dict[str, int] = {
|
LOG_LEVELS: dict[str, int] = {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from typing import Any
|
|||||||
from app.schemas import ModelType
|
from app.schemas import ModelType
|
||||||
|
|
||||||
from .base import InferenceModel
|
from .base import InferenceModel
|
||||||
from .clip import MCLIPEncoder, OpenCLIPEncoder, is_mclip, is_openclip
|
from .clip import MCLIPEncoder, OpenCLIPEncoder
|
||||||
|
from .constants import is_insightface, is_mclip, is_openclip
|
||||||
from .facial_recognition import FaceRecognizer
|
from .facial_recognition import FaceRecognizer
|
||||||
from .image_classification import ImageClassifier
|
from .image_classification import ImageClassifier
|
||||||
|
|
||||||
@@ -15,11 +16,12 @@ def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any)
|
|||||||
return OpenCLIPEncoder(model_name, **model_kwargs)
|
return OpenCLIPEncoder(model_name, **model_kwargs)
|
||||||
elif is_mclip(model_name):
|
elif is_mclip(model_name):
|
||||||
return MCLIPEncoder(model_name, **model_kwargs)
|
return MCLIPEncoder(model_name, **model_kwargs)
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown CLIP model {model_name}")
|
|
||||||
case ModelType.FACIAL_RECOGNITION:
|
case ModelType.FACIAL_RECOGNITION:
|
||||||
return FaceRecognizer(model_name, **model_kwargs)
|
if is_insightface(model_name):
|
||||||
|
return FaceRecognizer(model_name, **model_kwargs)
|
||||||
case ModelType.IMAGE_CLASSIFICATION:
|
case ModelType.IMAGE_CLASSIFICATION:
|
||||||
return ImageClassifier(model_name, **model_kwargs)
|
return ImageClassifier(model_name, **model_kwargs)
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unknown model type {model_type}")
|
raise ValueError(f"Unknown model type {model_type}")
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown ${model_type} model {model_name}")
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ from shutil import rmtree
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
|
from huggingface_hub import snapshot_download
|
||||||
|
|
||||||
from ..config import get_cache_dir, log, settings
|
from ..config import get_cache_dir, get_hf_model_name, log, settings
|
||||||
from ..schemas import ModelType
|
from ..schemas import ModelType
|
||||||
|
|
||||||
|
|
||||||
@@ -78,9 +79,13 @@ class InferenceModel(ABC):
|
|||||||
def configure(self, **model_kwargs: Any) -> None:
|
def configure(self, **model_kwargs: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _download(self) -> None:
|
def _download(self) -> None:
|
||||||
...
|
snapshot_download(
|
||||||
|
get_hf_model_name(self.model_name),
|
||||||
|
cache_dir=self.cache_dir,
|
||||||
|
local_dir=self.cache_dir,
|
||||||
|
local_dir_use_symlinks=False,
|
||||||
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ from typing import Any, Literal
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
from huggingface_hub import snapshot_download
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from transformers import AutoTokenizer
|
from transformers import AutoTokenizer
|
||||||
|
|
||||||
from app.config import log
|
from app.config import clean_name, log
|
||||||
from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy
|
from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy
|
||||||
from app.schemas import ModelType, ndarray_f32, ndarray_i32, ndarray_i64
|
from app.schemas import ModelType, ndarray_f32, ndarray_i32, ndarray_i64
|
||||||
|
|
||||||
@@ -117,15 +116,7 @@ class OpenCLIPEncoder(BaseCLIPEncoder):
|
|||||||
mode: Literal["text", "vision"] | None = None,
|
mode: Literal["text", "vision"] | None = None,
|
||||||
**model_kwargs: Any,
|
**model_kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(_clean_model_name(model_name), cache_dir, mode, **model_kwargs)
|
super().__init__(clean_name(model_name), cache_dir, mode, **model_kwargs)
|
||||||
|
|
||||||
def _download(self) -> None:
|
|
||||||
snapshot_download(
|
|
||||||
f"immich-app/{self.model_name}",
|
|
||||||
cache_dir=self.cache_dir,
|
|
||||||
local_dir=self.cache_dir,
|
|
||||||
local_dir_use_symlinks=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
super()._load()
|
super()._load()
|
||||||
@@ -171,52 +162,3 @@ class MCLIPEncoder(OpenCLIPEncoder):
|
|||||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||||
tokens: dict[str, ndarray_i64] = self.tokenizer(text, return_tensors="np")
|
tokens: dict[str, ndarray_i64] = self.tokenizer(text, return_tensors="np")
|
||||||
return {k: v.astype(np.int32) for k, v in tokens.items()}
|
return {k: v.astype(np.int32) for k, v in tokens.items()}
|
||||||
|
|
||||||
|
|
||||||
_OPENCLIP_MODELS = {
|
|
||||||
"RN50__openai",
|
|
||||||
"RN50__yfcc15m",
|
|
||||||
"RN50__cc12m",
|
|
||||||
"RN101__openai",
|
|
||||||
"RN101__yfcc15m",
|
|
||||||
"RN50x4__openai",
|
|
||||||
"RN50x16__openai",
|
|
||||||
"RN50x64__openai",
|
|
||||||
"ViT-B-32__openai",
|
|
||||||
"ViT-B-32__laion2b_e16",
|
|
||||||
"ViT-B-32__laion400m_e31",
|
|
||||||
"ViT-B-32__laion400m_e32",
|
|
||||||
"ViT-B-32__laion2b-s34b-b79k",
|
|
||||||
"ViT-B-16__openai",
|
|
||||||
"ViT-B-16__laion400m_e31",
|
|
||||||
"ViT-B-16__laion400m_e32",
|
|
||||||
"ViT-B-16-plus-240__laion400m_e31",
|
|
||||||
"ViT-B-16-plus-240__laion400m_e32",
|
|
||||||
"ViT-L-14__openai",
|
|
||||||
"ViT-L-14__laion400m_e31",
|
|
||||||
"ViT-L-14__laion400m_e32",
|
|
||||||
"ViT-L-14__laion2b-s32b-b82k",
|
|
||||||
"ViT-L-14-336__openai",
|
|
||||||
"ViT-H-14__laion2b-s32b-b79k",
|
|
||||||
"ViT-g-14__laion2b-s12b-b42k",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_MCLIP_MODELS = {
|
|
||||||
"LABSE-Vit-L-14",
|
|
||||||
"XLM-Roberta-Large-Vit-B-32",
|
|
||||||
"XLM-Roberta-Large-Vit-B-16Plus",
|
|
||||||
"XLM-Roberta-Large-Vit-L-14",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _clean_model_name(model_name: str) -> str:
|
|
||||||
return model_name.split("/")[-1].replace("::", "__")
|
|
||||||
|
|
||||||
|
|
||||||
def is_openclip(model_name: str) -> bool:
|
|
||||||
return _clean_model_name(model_name) in _OPENCLIP_MODELS
|
|
||||||
|
|
||||||
|
|
||||||
def is_mclip(model_name: str) -> bool:
|
|
||||||
return _clean_model_name(model_name) in _MCLIP_MODELS
|
|
||||||
|
|||||||
57
machine-learning/app/models/constants.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from app.config import clean_name
|
||||||
|
|
||||||
|
_OPENCLIP_MODELS = {
|
||||||
|
"RN50__openai",
|
||||||
|
"RN50__yfcc15m",
|
||||||
|
"RN50__cc12m",
|
||||||
|
"RN101__openai",
|
||||||
|
"RN101__yfcc15m",
|
||||||
|
"RN50x4__openai",
|
||||||
|
"RN50x16__openai",
|
||||||
|
"RN50x64__openai",
|
||||||
|
"ViT-B-32__openai",
|
||||||
|
"ViT-B-32__laion2b_e16",
|
||||||
|
"ViT-B-32__laion400m_e31",
|
||||||
|
"ViT-B-32__laion400m_e32",
|
||||||
|
"ViT-B-32__laion2b-s34b-b79k",
|
||||||
|
"ViT-B-16__openai",
|
||||||
|
"ViT-B-16__laion400m_e31",
|
||||||
|
"ViT-B-16__laion400m_e32",
|
||||||
|
"ViT-B-16-plus-240__laion400m_e31",
|
||||||
|
"ViT-B-16-plus-240__laion400m_e32",
|
||||||
|
"ViT-L-14__openai",
|
||||||
|
"ViT-L-14__laion400m_e31",
|
||||||
|
"ViT-L-14__laion400m_e32",
|
||||||
|
"ViT-L-14__laion2b-s32b-b82k",
|
||||||
|
"ViT-L-14-336__openai",
|
||||||
|
"ViT-H-14__laion2b-s32b-b79k",
|
||||||
|
"ViT-g-14__laion2b-s12b-b42k",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_MCLIP_MODELS = {
|
||||||
|
"LABSE-Vit-L-14",
|
||||||
|
"XLM-Roberta-Large-Vit-B-32",
|
||||||
|
"XLM-Roberta-Large-Vit-B-16Plus",
|
||||||
|
"XLM-Roberta-Large-Vit-L-14",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_INSIGHTFACE_MODELS = {
|
||||||
|
"antelopev2",
|
||||||
|
"buffalo_l",
|
||||||
|
"buffalo_m",
|
||||||
|
"buffalo_s",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_openclip(model_name: str) -> bool:
|
||||||
|
return clean_name(model_name) in _OPENCLIP_MODELS
|
||||||
|
|
||||||
|
|
||||||
|
def is_mclip(model_name: str) -> bool:
|
||||||
|
return clean_name(model_name) in _MCLIP_MODELS
|
||||||
|
|
||||||
|
|
||||||
|
def is_insightface(model_name: str) -> bool:
|
||||||
|
return clean_name(model_name) in _INSIGHTFACE_MODELS
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import zipfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -7,8 +6,8 @@ import numpy as np
|
|||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
from insightface.model_zoo import ArcFaceONNX, RetinaFace
|
from insightface.model_zoo import ArcFaceONNX, RetinaFace
|
||||||
from insightface.utils.face_align import norm_crop
|
from insightface.utils.face_align import norm_crop
|
||||||
from insightface.utils.storage import BASE_REPO_URL, download_file
|
|
||||||
|
|
||||||
|
from app.config import clean_name
|
||||||
from app.schemas import ModelType, ndarray_f32
|
from app.schemas import ModelType, ndarray_f32
|
||||||
|
|
||||||
from .base import InferenceModel
|
from .base import InferenceModel
|
||||||
@@ -25,37 +24,21 @@ class FaceRecognizer(InferenceModel):
|
|||||||
**model_kwargs: Any,
|
**model_kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.min_score = model_kwargs.pop("minScore", min_score)
|
self.min_score = model_kwargs.pop("minScore", min_score)
|
||||||
super().__init__(model_name, cache_dir, **model_kwargs)
|
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
|
||||||
|
|
||||||
def _download(self) -> None:
|
|
||||||
zip_file = self.cache_dir / f"{self.model_name}.zip"
|
|
||||||
download_file(f"{BASE_REPO_URL}/{self.model_name}.zip", zip_file)
|
|
||||||
with zipfile.ZipFile(zip_file, "r") as zip:
|
|
||||||
members = zip.namelist()
|
|
||||||
det_file = next(model for model in members if model.startswith("det_"))
|
|
||||||
rec_file = next(model for model in members if model.startswith("w600k_"))
|
|
||||||
zip.extractall(self.cache_dir, members=[det_file, rec_file])
|
|
||||||
zip_file.unlink()
|
|
||||||
|
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
try:
|
|
||||||
det_file = next(self.cache_dir.glob("det_*.onnx"))
|
|
||||||
rec_file = next(self.cache_dir.glob("w600k_*.onnx"))
|
|
||||||
except StopIteration:
|
|
||||||
raise FileNotFoundError("Facial recognition models not found in cache directory")
|
|
||||||
|
|
||||||
self.det_model = RetinaFace(
|
self.det_model = RetinaFace(
|
||||||
session=ort.InferenceSession(
|
session=ort.InferenceSession(
|
||||||
det_file.as_posix(),
|
self.det_file.as_posix(),
|
||||||
sess_options=self.sess_options,
|
sess_options=self.sess_options,
|
||||||
providers=self.providers,
|
providers=self.providers,
|
||||||
provider_options=self.provider_options,
|
provider_options=self.provider_options,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.rec_model = ArcFaceONNX(
|
self.rec_model = ArcFaceONNX(
|
||||||
rec_file.as_posix(),
|
self.rec_file.as_posix(),
|
||||||
session=ort.InferenceSession(
|
session=ort.InferenceSession(
|
||||||
rec_file.as_posix(),
|
self.rec_file.as_posix(),
|
||||||
sess_options=self.sess_options,
|
sess_options=self.sess_options,
|
||||||
providers=self.providers,
|
providers=self.providers,
|
||||||
provider_options=self.provider_options,
|
provider_options=self.provider_options,
|
||||||
@@ -103,7 +86,15 @@ class FaceRecognizer(InferenceModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def cached(self) -> bool:
|
def cached(self) -> bool:
|
||||||
return self.cache_dir.is_dir() and any(self.cache_dir.glob("*.onnx"))
|
return self.det_file.is_file() and self.rec_file.is_file()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def det_file(self) -> Path:
|
||||||
|
return self.cache_dir / "detection" / "model.onnx"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rec_file(self) -> Path:
|
||||||
|
return self.cache_dir / "recognition" / "model.onnx"
|
||||||
|
|
||||||
def configure(self, **model_kwargs: Any) -> None:
|
def configure(self, **model_kwargs: Any) -> None:
|
||||||
self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)
|
self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)
|
||||||
|
|||||||
@@ -106,13 +106,13 @@ class TestCLIP:
|
|||||||
class TestFaceRecognition:
|
class TestFaceRecognition:
|
||||||
def test_set_min_score(self, mocker: MockerFixture) -> None:
|
def test_set_min_score(self, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(FaceRecognizer, "load")
|
mocker.patch.object(FaceRecognizer, "load")
|
||||||
face_recognizer = FaceRecognizer("test_model_name", cache_dir="test_cache", min_score=0.5)
|
face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache", min_score=0.5)
|
||||||
|
|
||||||
assert face_recognizer.min_score == 0.5
|
assert face_recognizer.min_score == 0.5
|
||||||
|
|
||||||
def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
|
def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(FaceRecognizer, "load")
|
mocker.patch.object(FaceRecognizer, "load")
|
||||||
face_recognizer = FaceRecognizer("test_model_name", min_score=0.0, cache_dir="test_cache")
|
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
|
||||||
|
|
||||||
det_model = mock.Mock()
|
det_model = mock.Mock()
|
||||||
num_faces = 2
|
num_faces = 2
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ RUN micromamba install -y -n base -f /tmp/conda-lock.yml && \
|
|||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY --chown=$MAMBA_USER:$MAMBA_USER start.sh .
|
COPY --chown=$MAMBA_USER:$MAMBA_USER export .
|
||||||
COPY --chown=$MAMBA_USER:$MAMBA_USER app .
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
|
||||||
CMD ["./start.sh"]
|
CMD ["python -m run"]
|
||||||
|
|||||||
0
machine-learning/export/__init__.py
Normal file
9
machine-learning/export/models/constants.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from export.models.openclip import OpenCLIPModelConfig
|
||||||
|
|
||||||
|
|
||||||
|
MCLIP_TO_OPENCLIP = {
|
||||||
|
"XLM-Roberta-Large-Vit-B-32": OpenCLIPModelConfig("ViT-B-32", "openai"),
|
||||||
|
"XLM-Roberta-Large-Vit-B-16Plus": OpenCLIPModelConfig("ViT-B-16-plus-240", "laion400m_e32"),
|
||||||
|
"LABSE-Vit-L-14": OpenCLIPModelConfig("ViT-L-14", "openai"),
|
||||||
|
"XLM-Roberta-Large-Vit-L-14": OpenCLIPModelConfig("ViT-L-14", "openai"),
|
||||||
|
}
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from export.models.constants import MCLIP_TO_OPENCLIP
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
|
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
|
||||||
from transformers import AutoTokenizer
|
from transformers import AutoTokenizer
|
||||||
|
|
||||||
from .openclip import OpenCLIPModelConfig
|
|
||||||
from .openclip import to_onnx as openclip_to_onnx
|
from .openclip import to_onnx as openclip_to_onnx
|
||||||
from .optimize import optimize
|
from .optimize import optimize
|
||||||
from .util import get_model_path
|
from .util import get_model_path, clean_name
|
||||||
|
|
||||||
_MCLIP_TO_OPENCLIP = {
|
|
||||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32": OpenCLIPModelConfig("ViT-B-32", "openai"),
|
|
||||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus": OpenCLIPModelConfig("ViT-B-16-plus-240", "laion400m_e32"),
|
|
||||||
"M-CLIP/LABSE-Vit-L-14": OpenCLIPModelConfig("ViT-L-14", "openai"),
|
|
||||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14": OpenCLIPModelConfig("ViT-L-14", "openai"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def to_onnx(
|
def to_onnx(
|
||||||
@@ -33,7 +26,7 @@ def to_onnx(
|
|||||||
param.requires_grad_(False)
|
param.requires_grad_(False)
|
||||||
|
|
||||||
export_text_encoder(model, textual_path)
|
export_text_encoder(model, textual_path)
|
||||||
openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
|
openclip_to_onnx(MCLIP_TO_OPENCLIP[clean_name(model_name)], output_dir_visual)
|
||||||
optimize(textual_path)
|
optimize(textual_path)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
_clean_name = str.maketrans(":\\/", "___", ".")
|
||||||
|
|
||||||
|
|
||||||
def get_model_path(output_dir: Path | str) -> Path:
|
def get_model_path(output_dir: Path | str) -> Path:
|
||||||
output_dir = Path(output_dir)
|
output_dir = Path(output_dir)
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -13,3 +16,7 @@ def save_config(config: Any, output_path: Path | str) -> None:
|
|||||||
output_path = Path(output_path)
|
output_path = Path(output_path)
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
json.dump(config, output_path.open("w"))
|
json.dump(config, output_path.open("w"))
|
||||||
|
|
||||||
|
|
||||||
|
def clean_name(model_name: str) -> str:
|
||||||
|
return model_name.split("/")[-1].translate(_clean_name)
|
||||||
|
|||||||
@@ -1,76 +1,131 @@
|
|||||||
|
from enum import StrEnum
|
||||||
import gc
|
import gc
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from huggingface_hub import create_repo, login, upload_folder
|
from huggingface_hub import create_repo, upload_folder
|
||||||
from models import mclip, openclip
|
from export.models import mclip, openclip, insightface
|
||||||
|
from export.models.util import clean_name
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
|
import typer
|
||||||
|
|
||||||
models = [
|
|
||||||
"RN50::openai",
|
|
||||||
"RN50::yfcc15m",
|
|
||||||
"RN50::cc12m",
|
|
||||||
"RN101::openai",
|
|
||||||
"RN101::yfcc15m",
|
|
||||||
"RN50x4::openai",
|
|
||||||
"RN50x16::openai",
|
|
||||||
"RN50x64::openai",
|
|
||||||
"ViT-B-32::openai",
|
|
||||||
"ViT-B-32::laion2b_e16",
|
|
||||||
"ViT-B-32::laion400m_e31",
|
|
||||||
"ViT-B-32::laion400m_e32",
|
|
||||||
"ViT-B-32::laion2b-s34b-b79k",
|
|
||||||
"ViT-B-16::openai",
|
|
||||||
"ViT-B-16::laion400m_e31",
|
|
||||||
"ViT-B-16::laion400m_e32",
|
|
||||||
"ViT-B-16-plus-240::laion400m_e31",
|
|
||||||
"ViT-B-16-plus-240::laion400m_e32",
|
|
||||||
"ViT-L-14::openai",
|
|
||||||
"ViT-L-14::laion400m_e31",
|
|
||||||
"ViT-L-14::laion400m_e32",
|
|
||||||
"ViT-L-14::laion2b-s32b-b82k",
|
|
||||||
"ViT-L-14-336::openai",
|
|
||||||
"ViT-H-14::laion2b-s32b-b79k",
|
|
||||||
"ViT-g-14::laion2b-s12b-b42k",
|
|
||||||
"M-CLIP/LABSE-Vit-L-14",
|
|
||||||
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
|
|
||||||
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
|
|
||||||
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
|
|
||||||
]
|
|
||||||
|
|
||||||
login(token=os.environ["HF_AUTH_TOKEN"])
|
app = typer.Typer()
|
||||||
|
|
||||||
with Progress() as progress:
|
|
||||||
task1 = progress.add_task("[green]Exporting models...", total=len(models))
|
|
||||||
task2 = progress.add_task("[yellow]Uploading models...", total=len(models))
|
|
||||||
|
|
||||||
with TemporaryDirectory() as tmp:
|
class ModelLibrary(StrEnum):
|
||||||
tmpdir = Path(tmp)
|
MCLIP = "mclip"
|
||||||
for model in models:
|
OPENCLIP = "openclip"
|
||||||
model_name = model.split("/")[-1].replace("::", "__")
|
INSIGHTFACE = "insightface"
|
||||||
config_path = tmpdir / model_name / "config.json"
|
|
||||||
|
|
||||||
def upload() -> None:
|
|
||||||
progress.update(task2, description=f"[yellow]Uploading {model_name}")
|
|
||||||
repo_id = f"immich-app/{model_name}"
|
|
||||||
|
|
||||||
create_repo(repo_id, exist_ok=True)
|
def _export(model_name: str, library: ModelLibrary, export_dir: Path) -> None:
|
||||||
upload_folder(repo_id=repo_id, folder_path=tmpdir / model_name)
|
visual_dir = export_dir / "visual"
|
||||||
progress.update(task2, advance=1)
|
textual_dir = export_dir / "textual"
|
||||||
|
match library:
|
||||||
|
case ModelLibrary.MCLIP:
|
||||||
|
insightface.to_onnx(model_name, visual_dir, textual_dir)
|
||||||
|
case ModelLibrary.OPENCLIP:
|
||||||
|
mclip.to_onnx(model_name, visual_dir, textual_dir)
|
||||||
|
case ModelLibrary.INSIGHTFACE:
|
||||||
|
name, _, pretrained = model_name.partition("__")
|
||||||
|
openclip.to_onnx(openclip.OpenCLIPModelConfig(name, pretrained), visual_dir, textual_dir)
|
||||||
|
|
||||||
def export() -> None:
|
gc.collect()
|
||||||
progress.update(task1, description=f"[green]Exporting {model_name}")
|
|
||||||
visual_dir = tmpdir / model_name / "visual"
|
|
||||||
textual_dir = tmpdir / model_name / "textual"
|
|
||||||
if model.startswith("M-CLIP"):
|
|
||||||
mclip.to_onnx(model, visual_dir, textual_dir)
|
|
||||||
else:
|
|
||||||
name, _, pretrained = model_name.partition("__")
|
|
||||||
openclip.to_onnx(openclip.OpenCLIPModelConfig(name, pretrained), visual_dir, textual_dir)
|
|
||||||
|
|
||||||
progress.update(task1, advance=1)
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
export()
|
def _upload(repo_id: str, upload_dir: Path, auth_token: str | None = os.environ.get("HF_AUTH_TOKEN", None)) -> None:
|
||||||
upload()
|
create_repo(repo_id, exist_ok=True, token=auth_token)
|
||||||
|
upload_folder(repo_id=repo_id, folder_path=upload_dir, token=auth_token)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def export(
|
||||||
|
models: list[str] = typer.Argument(
|
||||||
|
..., help="The model(s) to be exported. Model names should be the same as used in the associated library."
|
||||||
|
),
|
||||||
|
library: ModelLibrary = typer.Option(
|
||||||
|
..., "--library", "-l", help="The library associated with the models to be exported."
|
||||||
|
),
|
||||||
|
output_dir: Optional[Path] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--output-dir",
|
||||||
|
"-o",
|
||||||
|
help="Directory where exported models will be stored. Defaults to a temporary directory.",
|
||||||
|
),
|
||||||
|
should_upload: bool = typer.Option(False, "--upload", "-u", help="Whether to upload the exported models."),
|
||||||
|
auth_token: Optional[str] = typer.Option(
|
||||||
|
os.environ.get("HF_AUTH_TOKEN", None),
|
||||||
|
"--auth_token",
|
||||||
|
"-t",
|
||||||
|
help="If uploading models to Hugging Face, the auth token of the user or organisation.",
|
||||||
|
),
|
||||||
|
repo_prefix: str = typer.Option(
|
||||||
|
"immich-app",
|
||||||
|
"--repo_prefix",
|
||||||
|
"-p",
|
||||||
|
help="If uploading models to Hugging Face, the prefix to put before the model name. Can be a username or organisation.",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
if not models:
|
||||||
|
raise ValueError("No models specified")
|
||||||
|
|
||||||
|
with Progress() as progress:
|
||||||
|
task1 = progress.add_task("[green]Exporting model(s)...", total=len(models))
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
output_dir = output_dir if output_dir else Path(tmp)
|
||||||
|
for model_name in models:
|
||||||
|
cleaned_name = clean_name(model_name)
|
||||||
|
model_dir = output_dir / cleaned_name
|
||||||
|
progress.update(task1, description=f"[green]Exporting {cleaned_name}")
|
||||||
|
_export(model_name, library, model_dir)
|
||||||
|
progress.update(task1, advance=1, description=f"[green]Exported {cleaned_name}")
|
||||||
|
|
||||||
|
if should_upload:
|
||||||
|
upload(models, output_dir, auth_token, repo_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def upload(
|
||||||
|
models: list[str] = typer.Argument(
|
||||||
|
..., help="The model(s) to be uploaded. Model names should be the same as used in the associated library."
|
||||||
|
),
|
||||||
|
output_dir: Optional[Path] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--output-dir",
|
||||||
|
"-o",
|
||||||
|
help="Directory where exported models will be stored. Defaults to a temporary directory.",
|
||||||
|
),
|
||||||
|
auth_token: Optional[str] = typer.Option(
|
||||||
|
os.environ.get("HF_AUTH_TOKEN", None),
|
||||||
|
"--auth_token",
|
||||||
|
"-t",
|
||||||
|
help="The Hugging Face auth token of the user or organisation.",
|
||||||
|
),
|
||||||
|
repo_prefix: str = typer.Option(
|
||||||
|
"immich-app",
|
||||||
|
"--repo_prefix",
|
||||||
|
"-p",
|
||||||
|
help="The name to put before the model name to form the Hugging Face repo name. Can be a username or organisation.",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
if not models:
|
||||||
|
raise ValueError("No models specified")
|
||||||
|
|
||||||
|
with Progress() as progress:
|
||||||
|
task2 = progress.add_task("[yellow]Uploading models...", total=len(models))
|
||||||
|
for model_name in models:
|
||||||
|
cleaned_name = clean_name(model_name)
|
||||||
|
repo_id = f"{repo_prefix}/{cleaned_name}"
|
||||||
|
model_dir = output_dir / cleaned_name
|
||||||
|
|
||||||
|
progress.update(task2, description=f"[yellow]Uploading {cleaned_name}")
|
||||||
|
_upload(repo_id, model_dir, auth_token)
|
||||||
|
progress.update(task2, advance=1, description=f"[yellow]Uploaded {cleaned_name}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.84.0"
|
version = "1.85.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 108,
|
"android.injected.version.code" => 109,
|
||||||
"android.injected.version.name" => "1.84.0",
|
"android.injected.version.name" => "1.85.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000625">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000244">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.943413">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.0562">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.374484">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.087498">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
||||||
"album_viewer_appbar_share_leave": "Leave album",
|
"album_viewer_appbar_share_leave": "Leave album",
|
||||||
"album_viewer_appbar_share_remove": "Remove from album",
|
"album_viewer_appbar_share_remove": "Remove from album",
|
||||||
|
"album_viewer_appbar_share_to": "Share To",
|
||||||
"album_viewer_page_share_add_users": "Add users",
|
"album_viewer_page_share_add_users": "Add users",
|
||||||
"all_people_page_title": "People",
|
"all_people_page_title": "People",
|
||||||
"all_videos_page_title": "Videos",
|
"all_videos_page_title": "Videos",
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||||
"cache_settings_title": "Caching Settings",
|
"cache_settings_title": "Caching Settings",
|
||||||
"change_password_form_confirm_password": "Confirm Password",
|
"change_password_form_confirm_password": "Confirm Password",
|
||||||
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
|
"change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
|
||||||
"change_password_form_new_password": "New Password",
|
"change_password_form_new_password": "New Password",
|
||||||
"change_password_form_password_mismatch": "Passwords do not match",
|
"change_password_form_password_mismatch": "Passwords do not match",
|
||||||
"change_password_form_reenter_new_password": "Re-enter New Password",
|
"change_password_form_reenter_new_password": "Re-enter New Password",
|
||||||
@@ -315,6 +316,7 @@
|
|||||||
"shared_link_edit_description": "Description",
|
"shared_link_edit_description": "Description",
|
||||||
"shared_link_edit_description_hint": "Enter the share description",
|
"shared_link_edit_description_hint": "Enter the share description",
|
||||||
"shared_link_edit_password": "Password",
|
"shared_link_edit_password": "Password",
|
||||||
|
"shared_link_edit_expire_after": "Expire after",
|
||||||
"shared_link_edit_password_hint": "Enter the share password",
|
"shared_link_edit_password_hint": "Enter the share password",
|
||||||
"shared_link_edit_show_meta": "Show metadata",
|
"shared_link_edit_show_meta": "Show metadata",
|
||||||
"shared_link_edit_submit_button": "Update link",
|
"shared_link_edit_submit_button": "Update link",
|
||||||
@@ -372,5 +374,11 @@
|
|||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"app_bar_signout_dialog_title": "Sign out",
|
"app_bar_signout_dialog_title": "Sign out",
|
||||||
"app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
|
"app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
|
||||||
"app_bar_signout_dialog_ok": "Yes"
|
"app_bar_signout_dialog_ok": "Yes",
|
||||||
|
"shared_album_activities_input_hint": "Say something",
|
||||||
|
"shared_album_activity_remove_title": "Delete Activity",
|
||||||
|
"shared_album_activity_remove_content": "Do you want to delete this activity?",
|
||||||
|
"shared_album_activity_setting_title": "Comments & likes",
|
||||||
|
"shared_album_activity_setting_subtitle": "Let others respond",
|
||||||
|
"shared_album_activities_input_disable": "Comment is disabled"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,7 +379,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 125;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 125;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 125;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -59,11 +59,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.84.0</string>
|
<string>1.85.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>124</string>
|
<string>125</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.84.0"
|
version_number: "1.85.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000291">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.181977">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.199372">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="16.12614">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.104477">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.162663">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.164465">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="145.399278">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="108.828838">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="61.317235">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="60.89387">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
54
mobile/lib/extensions/build_context_extensions.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension ContextHelper on BuildContext {
|
||||||
|
// Returns the current size from MediaQuery
|
||||||
|
Size get size => MediaQuery.sizeOf(this);
|
||||||
|
|
||||||
|
// Returns the current width from MediaQuery
|
||||||
|
double get width => size.width;
|
||||||
|
|
||||||
|
// Returns the current height from MediaQuery
|
||||||
|
double get height => size.height;
|
||||||
|
|
||||||
|
// Returns true if the app is running on a mobile device (!tablets)
|
||||||
|
bool get isMobile => width < 550;
|
||||||
|
|
||||||
|
// Returns the current ThemeData
|
||||||
|
ThemeData get themeData => Theme.of(this);
|
||||||
|
|
||||||
|
// Returns true if the app is using a dark theme
|
||||||
|
bool get isDarkTheme => themeData.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
// Returns the current Primary color of the Theme
|
||||||
|
Color get primaryColor => themeData.primaryColor;
|
||||||
|
|
||||||
|
// Returns the Scaffold background color of the Theme
|
||||||
|
Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor;
|
||||||
|
|
||||||
|
// Returns the current TextTheme
|
||||||
|
TextTheme get textTheme => themeData.textTheme;
|
||||||
|
|
||||||
|
// Current ColorScheme used
|
||||||
|
ColorScheme get colorScheme => themeData.colorScheme;
|
||||||
|
|
||||||
|
// Pop-out from the current context with optional result
|
||||||
|
void pop<T>([T? result]) => Navigator.of(this).pop(result);
|
||||||
|
|
||||||
|
// Auto-Push new route from the current context
|
||||||
|
Future<T?> autoPush<T extends Object?>(PageRouteInfo<dynamic> route) =>
|
||||||
|
AutoRouter.of(this).push(route);
|
||||||
|
|
||||||
|
// Auto-Push navigate route from the current context
|
||||||
|
Future<dynamic> autoNavigate<T extends Object?>(
|
||||||
|
PageRouteInfo<dynamic> route,
|
||||||
|
) =>
|
||||||
|
AutoRouter.of(this).navigate(route);
|
||||||
|
|
||||||
|
// Auto-Push replace route from the current context
|
||||||
|
Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
|
||||||
|
AutoRouter.of(this).replace(route);
|
||||||
|
|
||||||
|
// Auto-Pop from the current context
|
||||||
|
Future<bool> autoPop<T>([T? result]) => AutoRouter.of(this).pop(result);
|
||||||
|
}
|
||||||
@@ -2,27 +2,6 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
extension DurationExtension on String {
|
|
||||||
Duration? toDuration() {
|
|
||||||
try {
|
|
||||||
final parts = split(':')
|
|
||||||
.map((e) => double.parse(e).toInt())
|
|
||||||
.toList(growable: false);
|
|
||||||
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double toDouble() {
|
|
||||||
return double.parse(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
int toInt() {
|
|
||||||
return int.parse(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ListExtension<E> on List<E> {
|
extension ListExtension<E> on List<E> {
|
||||||
List<E> uniqueConsecutive({
|
List<E> uniqueConsecutive({
|
||||||
int Function(E a, E b)? compare,
|
int Function(E a, E b)? compare,
|
||||||
36
mobile/lib/extensions/datetime_extensions.dart
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
extension TimeAgoExtension on DateTime {
|
||||||
|
String timeAgo({bool numericDates = true}) {
|
||||||
|
DateTime date = toLocal();
|
||||||
|
final date2 = DateTime.now().toLocal();
|
||||||
|
final difference = date2.difference(date);
|
||||||
|
|
||||||
|
if (difference.inSeconds < 5) {
|
||||||
|
return 'Just now';
|
||||||
|
} else if (difference.inSeconds < 60) {
|
||||||
|
return '${difference.inSeconds} seconds ago';
|
||||||
|
} else if (difference.inMinutes <= 1) {
|
||||||
|
return (numericDates) ? '1 minute ago' : 'A minute ago';
|
||||||
|
} else if (difference.inMinutes < 60) {
|
||||||
|
return '${difference.inMinutes} minutes ago';
|
||||||
|
} else if (difference.inHours <= 1) {
|
||||||
|
return (numericDates) ? '1 hour ago' : 'An hour ago';
|
||||||
|
} else if (difference.inHours < 60) {
|
||||||
|
return '${difference.inHours} hours ago';
|
||||||
|
} else if (difference.inDays <= 1) {
|
||||||
|
return (numericDates) ? '1 day ago' : 'Yesterday';
|
||||||
|
} else if (difference.inDays < 6) {
|
||||||
|
return '${difference.inDays} days ago';
|
||||||
|
} else if ((difference.inDays / 7).ceil() <= 1) {
|
||||||
|
return (numericDates) ? '1 week ago' : 'Last week';
|
||||||
|
} else if ((difference.inDays / 7).ceil() < 4) {
|
||||||
|
return '${(difference.inDays / 7).ceil()} weeks ago';
|
||||||
|
} else if ((difference.inDays / 30).ceil() <= 1) {
|
||||||
|
return (numericDates) ? '1 month ago' : 'Last month';
|
||||||
|
} else if ((difference.inDays / 30).ceil() < 30) {
|
||||||
|
return '${(difference.inDays / 30).ceil()} months ago';
|
||||||
|
} else if ((difference.inDays / 365).ceil() <= 1) {
|
||||||
|
return (numericDates) ? '1 year ago' : 'Last year';
|
||||||
|
}
|
||||||
|
return '${(difference.inDays / 365).floor()} years ago';
|
||||||
|
}
|
||||||
|
}
|
||||||
30
mobile/lib/extensions/string_extensions.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
extension StringExtension on String {
|
||||||
|
String capitalize() {
|
||||||
|
return split(" ")
|
||||||
|
.map(
|
||||||
|
(str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1),
|
||||||
|
)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DurationExtension on String {
|
||||||
|
Duration? toDuration() {
|
||||||
|
try {
|
||||||
|
final parts = split(':')
|
||||||
|
.map((e) => double.parse(e).toInt())
|
||||||
|
.toList(growable: false);
|
||||||
|
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double toDouble() {
|
||||||
|
return double.parse(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
int toInt() {
|
||||||
|
return int.parse(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:timezone/data/latest.dart';
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
|
||||||
@@ -77,6 +78,8 @@ Future<void> initApp() async {
|
|||||||
log.severe('Catch all error: ${error.toString()} - $error', error, stack);
|
log.severe('Catch all error: ${error.toString()} - $error', error, stack);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
initializeTimeZones();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isar> loadDb() async {
|
Future<Isar> loadDb() async {
|
||||||
|
|||||||
89
mobile/lib/modules/activities/models/activity.model.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
enum ActivityType { comment, like }
|
||||||
|
|
||||||
|
class Activity {
|
||||||
|
final String id;
|
||||||
|
final String? assetId;
|
||||||
|
final String? comment;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final ActivityType type;
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
const Activity({
|
||||||
|
required this.id,
|
||||||
|
this.assetId,
|
||||||
|
this.comment,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.type,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
Activity copyWith({
|
||||||
|
String? id,
|
||||||
|
String? assetId,
|
||||||
|
String? comment,
|
||||||
|
DateTime? createdAt,
|
||||||
|
ActivityType? type,
|
||||||
|
User? user,
|
||||||
|
}) {
|
||||||
|
return Activity(
|
||||||
|
id: id ?? this.id,
|
||||||
|
assetId: assetId ?? this.assetId,
|
||||||
|
comment: comment ?? this.comment,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
type: type ?? this.type,
|
||||||
|
user: user ?? this.user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity.fromDto(ActivityResponseDto dto)
|
||||||
|
: id = dto.id,
|
||||||
|
assetId = dto.assetId,
|
||||||
|
comment = dto.comment,
|
||||||
|
createdAt = dto.createdAt,
|
||||||
|
type = dto.type == ActivityResponseDtoTypeEnum.comment
|
||||||
|
? ActivityType.comment
|
||||||
|
: ActivityType.like,
|
||||||
|
user = User(
|
||||||
|
email: dto.user.email,
|
||||||
|
name: dto.user.name,
|
||||||
|
profileImagePath: dto.user.profileImagePath,
|
||||||
|
id: dto.user.id,
|
||||||
|
// Placeholder values
|
||||||
|
isAdmin: false,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
isPartnerSharedBy: false,
|
||||||
|
isPartnerSharedWith: false,
|
||||||
|
memoryEnabled: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is Activity &&
|
||||||
|
other.id == id &&
|
||||||
|
other.assetId == assetId &&
|
||||||
|
other.comment == comment &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.type == type &&
|
||||||
|
other.user == user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
assetId.hashCode ^
|
||||||
|
comment.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
type.hashCode ^
|
||||||
|
user.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
130
mobile/lib/modules/activities/providers/activity.provider.dart
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/activities/services/activity.service.dart';
|
||||||
|
|
||||||
|
class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> {
|
||||||
|
final Ref _ref;
|
||||||
|
final ActivityService _activityService;
|
||||||
|
final String albumId;
|
||||||
|
final String? assetId;
|
||||||
|
|
||||||
|
ActivityNotifier(
|
||||||
|
this._ref,
|
||||||
|
this._activityService,
|
||||||
|
this.albumId,
|
||||||
|
this.assetId,
|
||||||
|
) : super(
|
||||||
|
const AsyncData([]),
|
||||||
|
) {
|
||||||
|
fetchActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchActivity() async {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = await AsyncValue.guard(
|
||||||
|
() => _activityService.getAllActivities(albumId, assetId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeActivity(String id) async {
|
||||||
|
final activities = state.asData?.value ?? [];
|
||||||
|
if (await _activityService.removeActivity(id)) {
|
||||||
|
final removedActivity = activities.firstWhere((a) => a.id == id);
|
||||||
|
activities.remove(removedActivity);
|
||||||
|
state = AsyncData(activities);
|
||||||
|
if (removedActivity.type == ActivityType.comment) {
|
||||||
|
_ref
|
||||||
|
.read(
|
||||||
|
activityStatisticsStateProvider(
|
||||||
|
(albumId: albumId, assetId: assetId),
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.removeActivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addComment(String comment) async {
|
||||||
|
final activity = await _activityService.addActivity(
|
||||||
|
albumId,
|
||||||
|
ActivityType.comment,
|
||||||
|
assetId: assetId,
|
||||||
|
comment: comment,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activity != null) {
|
||||||
|
final activities = state.asData?.value ?? [];
|
||||||
|
state = AsyncData([...activities, activity]);
|
||||||
|
_ref
|
||||||
|
.read(
|
||||||
|
activityStatisticsStateProvider(
|
||||||
|
(albumId: albumId, assetId: assetId),
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.addActivity();
|
||||||
|
if (assetId != null) {
|
||||||
|
// Add a count to the current album's provider as well
|
||||||
|
_ref
|
||||||
|
.read(
|
||||||
|
activityStatisticsStateProvider(
|
||||||
|
(albumId: albumId, assetId: null),
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.addActivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addLike() async {
|
||||||
|
final activity = await _activityService
|
||||||
|
.addActivity(albumId, ActivityType.like, assetId: assetId);
|
||||||
|
if (activity != null) {
|
||||||
|
final activities = state.asData?.value ?? [];
|
||||||
|
state = AsyncData([...activities, activity]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityStatisticsNotifier extends StateNotifier<int> {
|
||||||
|
final String albumId;
|
||||||
|
final String? assetId;
|
||||||
|
final ActivityService _activityService;
|
||||||
|
ActivityStatisticsNotifier(this._activityService, this.albumId, this.assetId)
|
||||||
|
: super(0) {
|
||||||
|
fetchStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchStatistics() async {
|
||||||
|
state = await _activityService.getStatistics(albumId, assetId: assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addActivity() async {
|
||||||
|
state = state + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeActivity() async {
|
||||||
|
state = state - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef ActivityParams = ({String albumId, String? assetId});
|
||||||
|
|
||||||
|
final activityStateProvider = StateNotifierProvider.autoDispose
|
||||||
|
.family<ActivityNotifier, AsyncValue<List<Activity>>, ActivityParams>(
|
||||||
|
(ref, args) {
|
||||||
|
return ActivityNotifier(
|
||||||
|
ref,
|
||||||
|
ref.watch(activityServiceProvider),
|
||||||
|
args.albumId,
|
||||||
|
args.assetId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final activityStatisticsStateProvider = StateNotifierProvider.autoDispose
|
||||||
|
.family<ActivityStatisticsNotifier, int, ActivityParams>((ref, args) {
|
||||||
|
return ActivityStatisticsNotifier(
|
||||||
|
ref.watch(activityServiceProvider),
|
||||||
|
args.albumId,
|
||||||
|
args.assetId,
|
||||||
|
);
|
||||||
|
});
|
||||||
85
mobile/lib/modules/activities/services/activity.service.dart
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final activityServiceProvider =
|
||||||
|
Provider((ref) => ActivityService(ref.watch(apiServiceProvider)));
|
||||||
|
|
||||||
|
class ActivityService {
|
||||||
|
final ApiService _apiService;
|
||||||
|
final Logger _log = Logger("ActivityService");
|
||||||
|
|
||||||
|
ActivityService(this._apiService);
|
||||||
|
|
||||||
|
Future<List<Activity>> getAllActivities(
|
||||||
|
String albumId,
|
||||||
|
String? assetId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final list = await _apiService.activityApi
|
||||||
|
.getActivities(albumId, assetId: assetId);
|
||||||
|
return list != null ? list.map(Activity.fromDto).toList() : [];
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(
|
||||||
|
"failed to fetch activities for albumId - $albumId; assetId - $assetId -> $e",
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getStatistics(String albumId, {String? assetId}) async {
|
||||||
|
try {
|
||||||
|
final dto = await _apiService.activityApi
|
||||||
|
.getActivityStatistics(albumId, assetId: assetId);
|
||||||
|
return dto?.comments ?? 0;
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(
|
||||||
|
"failed to fetch activity statistics for albumId - $albumId; assetId - $assetId -> $e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeActivity(String id) async {
|
||||||
|
try {
|
||||||
|
await _apiService.activityApi.deleteActivity(id);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(
|
||||||
|
"failed to remove activity id - $id -> $e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Activity?> addActivity(
|
||||||
|
String albumId,
|
||||||
|
ActivityType type, {
|
||||||
|
String? assetId,
|
||||||
|
String? comment,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final dto = await _apiService.activityApi.createActivity(
|
||||||
|
ActivityCreateDto(
|
||||||
|
albumId: albumId,
|
||||||
|
type: type == ActivityType.comment
|
||||||
|
? ReactionType.comment
|
||||||
|
: ReactionType.like,
|
||||||
|
assetId: assetId,
|
||||||
|
comment: comment,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (dto != null) {
|
||||||
|
return Activity.fromDto(dto);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(
|
||||||
|
"failed to add activity for albumId - $albumId; assetId - $assetId -> $e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
317
mobile/lib/modules/activities/views/activities_page.dart
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||||
|
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
|
class ActivitiesPage extends HookConsumerWidget {
|
||||||
|
final String albumId;
|
||||||
|
final String? assetId;
|
||||||
|
final bool withAssetThumbs;
|
||||||
|
final String appBarTitle;
|
||||||
|
final bool isOwner;
|
||||||
|
final bool isReadOnly;
|
||||||
|
const ActivitiesPage(
|
||||||
|
this.albumId, {
|
||||||
|
this.appBarTitle = "",
|
||||||
|
this.assetId,
|
||||||
|
this.withAssetThumbs = true,
|
||||||
|
this.isOwner = false,
|
||||||
|
this.isReadOnly = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final provider =
|
||||||
|
activityStateProvider((albumId: albumId, assetId: assetId));
|
||||||
|
final activities = ref.watch(provider);
|
||||||
|
final inputController = useTextEditingController();
|
||||||
|
final inputFocusNode = useFocusNode();
|
||||||
|
final listViewScrollController = useScrollController();
|
||||||
|
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
inputFocusNode.requestFocus();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) {
|
||||||
|
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||||
|
final textStyle = context.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: textColor.withOpacity(0.6));
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: leftAlign
|
||||||
|
? MainAxisAlignment.start
|
||||||
|
: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
activity.user.name,
|
||||||
|
style: textStyle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (leftAlign)
|
||||||
|
Text(
|
||||||
|
" • ",
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
activity.createdAt.copyWith().timeAgo(),
|
||||||
|
style: textStyle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: leftAlign ? TextAlign.left : TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAssetThumbnail(Activity activity) {
|
||||||
|
return withAssetThumbs && activity.assetId != null
|
||||||
|
? Container(
|
||||||
|
width: 40,
|
||||||
|
height: 30,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
image: DecorationImage(
|
||||||
|
image: CachedNetworkImageProvider(
|
||||||
|
getThumbnailUrlForRemoteId(
|
||||||
|
activity.assetId!,
|
||||||
|
),
|
||||||
|
cacheKey: getThumbnailCacheKeyForRemoteId(
|
||||||
|
activity.assetId!,
|
||||||
|
),
|
||||||
|
headers: {
|
||||||
|
"Authorization":
|
||||||
|
'Bearer ${Store.get(StoreKey.accessToken)}',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const SizedBox.shrink(),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTextField(String? likedId) {
|
||||||
|
final liked = likedId != null;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: TextField(
|
||||||
|
controller: inputController,
|
||||||
|
enabled: !isReadOnly,
|
||||||
|
focusNode: inputFocusNode,
|
||||||
|
textInputAction: TextInputAction.send,
|
||||||
|
autofocus: false,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
prefixIcon: currentUser != null
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: UserCircleAvatar(
|
||||||
|
user: currentUser,
|
||||||
|
size: 30,
|
||||||
|
radius: 15,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
liked
|
||||||
|
? Icons.favorite_rounded
|
||||||
|
: Icons.favorite_border_rounded,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
liked
|
||||||
|
? await ref
|
||||||
|
.read(provider.notifier)
|
||||||
|
.removeActivity(likedId)
|
||||||
|
: await ref.read(provider.notifier).addLike();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffixIconColor: liked ? Colors.red[700] : null,
|
||||||
|
hintText: isReadOnly
|
||||||
|
? 'shared_album_activities_input_disable'.tr()
|
||||||
|
: 'shared_album_activities_input_hint'.tr(),
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onEditingComplete: () async {
|
||||||
|
await ref.read(provider.notifier).addComment(inputController.text);
|
||||||
|
inputController.clear();
|
||||||
|
inputFocusNode.unfocus();
|
||||||
|
listViewScrollController.animateTo(
|
||||||
|
listViewScrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onTapOutside: (_) => inputFocusNode.unfocus(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDismissibleWidget(
|
||||||
|
Widget widget,
|
||||||
|
Activity activity,
|
||||||
|
bool canDelete,
|
||||||
|
) {
|
||||||
|
return Dismissible(
|
||||||
|
key: Key(activity.id),
|
||||||
|
dismissThresholds: const {
|
||||||
|
DismissDirection.horizontal: 0.7,
|
||||||
|
},
|
||||||
|
direction: DismissDirection.horizontal,
|
||||||
|
confirmDismiss: (direction) => canDelete
|
||||||
|
? showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfirmDialog(
|
||||||
|
onOk: () {},
|
||||||
|
title: "shared_album_activity_remove_title",
|
||||||
|
content: "shared_album_activity_remove_content",
|
||||||
|
ok: "delete_dialog_ok",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Future.value(false),
|
||||||
|
onDismissed: (direction) async =>
|
||||||
|
await ref.read(provider.notifier).removeActivity(activity.id),
|
||||||
|
background: Container(
|
||||||
|
color: canDelete ? Colors.red[400] : Colors.grey[600],
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: canDelete
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: Icon(
|
||||||
|
Icons.delete_sweep_rounded,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
secondaryBackground: Container(
|
||||||
|
color: canDelete ? Colors.red[400] : Colors.grey[600],
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: canDelete
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: Icon(
|
||||||
|
Icons.delete_sweep_rounded,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: widget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(appBarTitle)),
|
||||||
|
body: activities.maybeWhen(
|
||||||
|
orElse: () {
|
||||||
|
return const Center(child: ImmichLoadingIndicator());
|
||||||
|
},
|
||||||
|
data: (data) {
|
||||||
|
final liked = data.firstWhereOrNull(
|
||||||
|
(a) =>
|
||||||
|
a.type == ActivityType.like &&
|
||||||
|
a.user.id == currentUser?.id &&
|
||||||
|
a.assetId == assetId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ListView.builder(
|
||||||
|
controller: listViewScrollController,
|
||||||
|
itemCount: data.length + 1,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
// Vertical gap after the last element
|
||||||
|
if (index == data.length) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 80,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final activity = data[index];
|
||||||
|
final canDelete =
|
||||||
|
activity.user.id == currentUser?.id || isOwner;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: activity.type == ActivityType.comment
|
||||||
|
? getDismissibleWidget(
|
||||||
|
ListTile(
|
||||||
|
minVerticalPadding: 15,
|
||||||
|
leading: UserCircleAvatar(user: activity.user),
|
||||||
|
title: buildTitleWithTimestamp(
|
||||||
|
activity,
|
||||||
|
leftAlign: withAssetThumbs &&
|
||||||
|
activity.assetId != null,
|
||||||
|
),
|
||||||
|
titleAlignment: ListTileTitleAlignment.top,
|
||||||
|
trailing: buildAssetThumbnail(activity),
|
||||||
|
subtitle: Text(activity.comment!),
|
||||||
|
),
|
||||||
|
activity,
|
||||||
|
canDelete,
|
||||||
|
)
|
||||||
|
: getDismissibleWidget(
|
||||||
|
ListTile(
|
||||||
|
minVerticalPadding: 15,
|
||||||
|
leading: Container(
|
||||||
|
width: 44,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Icon(
|
||||||
|
Icons.favorite_rounded,
|
||||||
|
color: Colors.red[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: buildTitleWithTimestamp(activity),
|
||||||
|
trailing: buildAssetThumbnail(activity),
|
||||||
|
),
|
||||||
|
activity,
|
||||||
|
canDelete,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Container(
|
||||||
|
color: context.scaffoldBackgroundColor,
|
||||||
|
child: buildTextField(liked?.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
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/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@@ -10,7 +11,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
|
|||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
||||||
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
|
SharedAlbumNotifier(this._albumService, Isar db, this._ref) : super([]) {
|
||||||
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
|
||||||
query.findAll().then((value) => state = value);
|
query.findAll().then((value) => state = value);
|
||||||
_streamSub = query.watch().listen((data) => state = data);
|
_streamSub = query.watch().listen((data) => state = data);
|
||||||
@@ -18,6 +19,7 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
|
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
late final StreamSubscription<List<Album>> _streamSub;
|
late final StreamSubscription<List<Album>> _streamSub;
|
||||||
|
final Ref _ref;
|
||||||
|
|
||||||
Future<Album?> createSharedAlbum(
|
Future<Album?> createSharedAlbum(
|
||||||
String albumName,
|
String albumName,
|
||||||
@@ -66,6 +68,17 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setActivityEnabled(Album album, bool activityEnabled) async {
|
||||||
|
final result =
|
||||||
|
await _albumService.setActivityEnabled(album, activityEnabled);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
_ref.invalidate(albumDetailProvider(album.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamSub.cancel();
|
_streamSub.cancel();
|
||||||
@@ -78,5 +91,6 @@ final sharedAlbumProvider =
|
|||||||
return SharedAlbumNotifier(
|
return SharedAlbumNotifier(
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -284,6 +284,23 @@ class AlbumService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
||||||
|
try {
|
||||||
|
final result = await _apiService.albumApi.updateAlbumInfo(
|
||||||
|
album.remoteId!,
|
||||||
|
UpdateAlbumDto(isActivityEnabled: enabled),
|
||||||
|
);
|
||||||
|
if (result != null) {
|
||||||
|
album.activityEnabled = enabled;
|
||||||
|
await _db.writeTxn(() => _db.albums.put(album));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> deleteAlbum(Album album) async {
|
Future<bool> deleteAlbum(Album album) async {
|
||||||
try {
|
try {
|
||||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
@@ -95,20 +95,19 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'common_add_to_album'.tr(),
|
'common_add_to_album'.tr(),
|
||||||
style: Theme.of(context).textTheme.displayMedium,
|
style: context.textTheme.displayMedium,
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
'common_create_new_album'.tr(),
|
'common_create_new_album'.tr(),
|
||||||
style:
|
style: TextStyle(color: context.primaryColor),
|
||||||
TextStyle(color: Theme.of(context).primaryColor),
|
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(
|
context.autoPush(
|
||||||
CreateAlbumRoute(
|
CreateAlbumRoute(
|
||||||
isSharedAlbum: false,
|
isSharedAlbum: false,
|
||||||
initialAssets: assets,
|
initialAssets: assets,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class AlbumActionOutlinedButton extends StatelessWidget {
|
class AlbumActionOutlinedButton extends StatelessWidget {
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
@@ -14,8 +15,6 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
@@ -26,7 +25,7 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
width: 1,
|
width: 1,
|
||||||
color: isDarkTheme
|
color: context.isDarkTheme
|
||||||
? const Color.fromARGB(255, 63, 63, 63)
|
? const Color.fromARGB(255, 63, 63, 63)
|
||||||
: const Color.fromARGB(255, 206, 206, 206),
|
: const Color.fromARGB(255, 206, 206, 206),
|
||||||
),
|
),
|
||||||
@@ -34,13 +33,13 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
iconData,
|
iconData,
|
||||||
size: 15,
|
size: 15,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
labelText,
|
labelText,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: context.textTheme.labelSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
@@ -22,7 +23,8 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
var isDarkTheme = context.isDarkTheme;
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
var cardSize = constraints.maxWidth;
|
var cardSize = constraints.maxWidth;
|
||||||
@@ -32,7 +34,7 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
height: cardSize,
|
height: cardSize,
|
||||||
width: cardSize,
|
width: cardSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
|
color: isDarkTheme ? Colors.grey[800] : Colors.grey[200],
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -73,14 +75,14 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: isDarkMode ? Colors.white : Colors.black,
|
color: isDarkTheme ? Colors.white : Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (owner != null) const TextSpan(text: ' · '),
|
if (owner != null) const TextSpan(text: ' · '),
|
||||||
if (owner != null)
|
if (owner != null)
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: owner,
|
text: owner,
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: context.textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -114,8 +116,8 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
album.name,
|
album.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isDarkMode
|
color: isDarkTheme
|
||||||
? Theme.of(context).primaryColor
|
? context.primaryColor
|
||||||
: Colors.black,
|
: Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
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:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
@@ -21,12 +21,11 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var cardSize = 68.0;
|
var cardSize = 68.0;
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
buildEmptyThumbnail() {
|
buildEmptyThumbnail() {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
|
color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200],
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: cardSize,
|
height: cardSize,
|
||||||
@@ -61,7 +60,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
|||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: onTap ??
|
onTap: onTap ??
|
||||||
() {
|
() {
|
||||||
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
|
context.autoPush(AlbumViewerRoute(albumId: album.id));
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12.0),
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
|
|
||||||
class AlbumTitleTextField extends ConsumerWidget {
|
class AlbumTitleTextField extends ConsumerWidget {
|
||||||
@@ -19,7 +20,7 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
@@ -55,7 +56,7 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.cancel_rounded,
|
Icons.cancel_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
splashRadius: 10,
|
splashRadius: 10,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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/modules/activities/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@@ -24,6 +27,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
required this.titleFocusNode,
|
required this.titleFocusNode,
|
||||||
this.onAddPhotos,
|
this.onAddPhotos,
|
||||||
this.onAddUsers,
|
this.onAddUsers,
|
||||||
|
required this.onActivities,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Album album;
|
final Album album;
|
||||||
@@ -33,11 +37,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
final Function(Album album)? onAddPhotos;
|
final Function(Album album)? onAddPhotos;
|
||||||
final Function(Album album)? onAddUsers;
|
final Function(Album album)? onAddUsers;
|
||||||
|
final Function(Album album) onActivities;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
final comments = album.shared
|
||||||
|
? ref.watch(
|
||||||
|
activityStatisticsStateProvider(
|
||||||
|
(albumId: album.remoteId!, assetId: null),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
deleteAlbum() async {
|
deleteAlbum() async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
@@ -46,12 +58,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
if (album.shared) {
|
if (album.shared) {
|
||||||
success =
|
success =
|
||||||
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
|
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
|
||||||
AutoRouter.of(context)
|
context
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||||
AutoRouter.of(context)
|
context
|
||||||
.navigate(const TabControllerRoute(children: [LibraryRoute()]));
|
.autoNavigate(const TabControllerRoute(children: [LibraryRoute()]));
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -81,7 +93,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Cancel',
|
'Cancel',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -95,9 +107,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
'Confirm',
|
'Confirm',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
color: !context.isDarkTheme ? Colors.red : Colors.red[300],
|
||||||
? Colors.red
|
|
||||||
: Colors.red[300],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -118,8 +128,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context)
|
context
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -160,40 +170,81 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
ImmichLoadingOverlayController.appLoader.hide();
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
buildBottomSheetActionButton() {
|
void handleShareAssets(
|
||||||
|
WidgetRef ref,
|
||||||
|
BuildContext context,
|
||||||
|
Set<Asset> selection,
|
||||||
|
) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext buildContext) {
|
||||||
|
ref.watch(shareServiceProvider).shareAssets(selection.toList()).then(
|
||||||
|
(bool status) {
|
||||||
|
if (!status) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: 'image_viewer_page_state_provider_share_error'.tr(),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return const ShareDialog();
|
||||||
|
},
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onShareAssetsTo() async {
|
||||||
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
|
handleShareAssets(ref, context, selected);
|
||||||
|
ImmichLoadingOverlayController.appLoader.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildBottomSheetActions() {
|
||||||
if (selected.isNotEmpty) {
|
if (selected.isNotEmpty) {
|
||||||
if (album.ownerId == userId) {
|
return [
|
||||||
return ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep_rounded),
|
leading: const Icon(Icons.ios_share_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'album_viewer_appbar_share_remove',
|
'album_viewer_appbar_share_to',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => onRemoveFromAlbumPressed(),
|
onTap: () => onShareAssetsTo(),
|
||||||
);
|
),
|
||||||
} else {
|
album.ownerId == userId
|
||||||
return const SizedBox();
|
? ListTile(
|
||||||
}
|
leading: const Icon(Icons.delete_sweep_rounded),
|
||||||
|
title: const Text(
|
||||||
|
'album_viewer_appbar_share_remove',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
onTap: () => onRemoveFromAlbumPressed(),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
if (album.ownerId == userId) {
|
return [
|
||||||
return ListTile(
|
album.ownerId == userId
|
||||||
leading: const Icon(Icons.delete_forever_rounded),
|
? ListTile(
|
||||||
title: const Text(
|
leading: const Icon(Icons.delete_forever_rounded),
|
||||||
'album_viewer_appbar_share_delete',
|
title: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
'album_viewer_appbar_share_delete',
|
||||||
).tr(),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
onTap: () => onDeleteAlbumPressed(),
|
).tr(),
|
||||||
);
|
onTap: () => onDeleteAlbumPressed(),
|
||||||
} else {
|
)
|
||||||
return ListTile(
|
: ListTile(
|
||||||
leading: const Icon(Icons.person_remove_rounded),
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'album_viewer_appbar_share_leave',
|
'album_viewer_appbar_share_leave',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
).tr(),
|
).tr(),
|
||||||
onTap: () => onLeaveAlbumPressed(),
|
onTap: () => onLeaveAlbumPressed(),
|
||||||
);
|
),
|
||||||
}
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +264,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.share_rounded),
|
leading: const Icon(Icons.share_rounded),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context)
|
context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
|
||||||
.push(SharedLinkEditRoute(albumId: album.remoteId));
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@@ -224,8 +274,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings_rounded),
|
leading: const Icon(Icons.settings_rounded),
|
||||||
onTap: () =>
|
onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
|
||||||
AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)),
|
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"translated_text_options",
|
"translated_text_options",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
@@ -247,7 +296,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -257,7 +306,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
buildBottomSheetActionButton(),
|
...buildBottomSheetActions(),
|
||||||
if (selected.isEmpty && onAddPhotos != null) ...commonActions,
|
if (selected.isEmpty && onAddPhotos != null) ...commonActions,
|
||||||
if (selected.isEmpty &&
|
if (selected.isEmpty &&
|
||||||
onAddPhotos != null &&
|
onAddPhotos != null &&
|
||||||
@@ -271,6 +320,33 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildActivitiesButton() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
onActivities(album);
|
||||||
|
},
|
||||||
|
icon: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.mode_comment_outlined,
|
||||||
|
),
|
||||||
|
if (comments != 0)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Text(
|
||||||
|
comments.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
buildLeadingButton() {
|
buildLeadingButton() {
|
||||||
if (selected.isNotEmpty) {
|
if (selected.isNotEmpty) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
@@ -301,7 +377,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async => await AutoRouter.of(context).pop(),
|
onPressed: () async => await context.autoPop(),
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
@@ -314,6 +390,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
|||||||
title: selected.isNotEmpty ? Text('${selected.length}') : null,
|
title: selected.isNotEmpty ? Text('${selected.length}') : null,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
|
if (album.shared && (album.activityEnabled || comments != 0))
|
||||||
|
buildActivitiesButton(),
|
||||||
if (album.isRemote)
|
if (album.isRemote)
|
||||||
IconButton(
|
IconButton(
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
|
|||||||
@@ -2,6 +2,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';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final titleTextEditController = useTextEditingController(text: album.name);
|
final titleTextEditController = useTextEditingController(text: album.name);
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
void onFocusModeChange() {
|
void onFocusModeChange() {
|
||||||
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
||||||
@@ -65,7 +65,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.cancel_rounded,
|
Icons.cancel_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
splashRadius: 10,
|
splashRadius: 10,
|
||||||
)
|
)
|
||||||
@@ -79,14 +79,14 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: isDarkTheme
|
fillColor: context.isDarkTheme
|
||||||
? const Color.fromARGB(255, 32, 33, 35)
|
? const Color.fromARGB(255, 32, 33, 35)
|
||||||
: Colors.grey[200],
|
: Colors.grey[200],
|
||||||
filled: titleFocusNode.hasFocus,
|
filled: titleFocusNode.hasFocus,
|
||||||
hintText: 'share_add_title'.tr(),
|
hintText: 'share_add_title'.tr(),
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -23,6 +23,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
final sharedUsers = useState(album.sharedUsers.toList());
|
final sharedUsers = useState(album.sharedUsers.toList());
|
||||||
final owner = album.owner.value;
|
final owner = album.owner.value;
|
||||||
final userId = ref.watch(authenticationProvider).userId;
|
final userId = ref.watch(authenticationProvider).userId;
|
||||||
|
final activityEnabled = useState(album.activityEnabled);
|
||||||
final isOwner = owner?.id == userId;
|
final isOwner = owner?.id == userId;
|
||||||
|
|
||||||
void showErrorMessage() {
|
void showErrorMessage() {
|
||||||
@@ -43,8 +44,9 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
AutoRouter.of(context)
|
context.autoNavigate(
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
const TabControllerRoute(children: [SharingRoute()]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showErrorMessage();
|
showErrorMessage();
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@@ -122,7 +124,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: const SizedBox(),
|
: const SizedBox(),
|
||||||
title: Text(
|
title: Text(
|
||||||
album.owner.value?.firstName ?? "",
|
album.owner.value?.name ?? "",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -153,7 +155,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
radius: 22,
|
radius: 22,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
user.firstName,
|
user.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -176,7 +178,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
buildSectionTitle(String text) {
|
buildSectionTitle(String text) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(text, style: Theme.of(context).textTheme.bodySmall),
|
child: Text(text, style: context.textTheme.bodySmall),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +187,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop(null);
|
context.autoPop(null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
@@ -195,6 +197,29 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (isOwner && album.shared)
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: activityEnabled.value,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
activityEnabled.value = value;
|
||||||
|
if (await ref
|
||||||
|
.read(sharedAlbumProvider.notifier)
|
||||||
|
.setActivityEnabled(album, value)) {
|
||||||
|
album.activityEnabled = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeColor: activityEnabled.value
|
||||||
|
? context.primaryColor
|
||||||
|
: context.themeData.disabledColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"shared_album_activity_setting_title",
|
||||||
|
style: context.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
subtitle:
|
||||||
|
const Text("shared_album_activity_setting_subtitle").tr(),
|
||||||
|
),
|
||||||
buildSectionTitle("PEOPLE"),
|
buildSectionTitle("PEOPLE"),
|
||||||
buildOwnerInfo(),
|
buildOwnerInfo(),
|
||||||
buildSharedUsersList(),
|
buildSharedUsersList(),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
@@ -67,7 +67,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
/// If they exist, add to selected asset state to show they are already selected.
|
/// If they exist, add to selected asset state to show they are already selected.
|
||||||
void onAddPhotosPressed(Album albumInfo) async {
|
void onAddPhotosPressed(Album albumInfo) async {
|
||||||
AssetSelectionPageResult? returnPayload =
|
AssetSelectionPageResult? returnPayload =
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
await context.autoPush<AssetSelectionPageResult?>(
|
||||||
AssetSelectionRoute(
|
AssetSelectionRoute(
|
||||||
existingAssets: albumInfo.assets,
|
existingAssets: albumInfo.assets,
|
||||||
canDeselect: false,
|
canDeselect: false,
|
||||||
@@ -97,8 +97,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onAddUsersPressed(Album album) async {
|
void onAddUsersPressed(Album album) async {
|
||||||
List<String>? sharedUserIds =
|
List<String>? sharedUserIds = await context.autoPush<List<String>?>(
|
||||||
await AutoRouter.of(context).push<List<String>?>(
|
|
||||||
SelectAdditionalUserForSharingRoute(album: album),
|
SelectAdditionalUserForSharingRoute(album: album),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -171,11 +170,19 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String startDateText = (startDate.year == endDate.year
|
final String dateRangeText;
|
||||||
? DateFormat.MMMd()
|
if (startDate.day == endDate.day &&
|
||||||
: DateFormat.yMMMd())
|
startDate.month == endDate.month &&
|
||||||
.format(startDate);
|
startDate.year == endDate.year) {
|
||||||
final String endDateText = DateFormat.yMMMd().format(endDate);
|
dateRangeText = DateFormat.yMMMd().format(startDate);
|
||||||
|
} else {
|
||||||
|
final String startDateText = (startDate.year == endDate.year
|
||||||
|
? DateFormat.MMMd()
|
||||||
|
: DateFormat.yMMMd())
|
||||||
|
.format(startDate);
|
||||||
|
final String endDateText = DateFormat.yMMMd().format(endDate);
|
||||||
|
dateRangeText = "$startDateText - $endDateText";
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
@@ -183,7 +190,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
bottom: album.shared ? 0.0 : 8.0,
|
bottom: album.shared ? 0.0 : 8.0,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"$startDateText - $endDateText",
|
dateRangeText,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -195,7 +202,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
Widget buildSharedUserIconsRow(Album album) {
|
Widget buildSharedUserIconsRow(Album album) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await AutoRouter.of(context).push(AlbumOptionsRoute(album: album));
|
await context.autoPush(AlbumOptionsRoute(album: album));
|
||||||
ref.invalidate(albumDetailProvider(album.id));
|
ref.invalidate(albumDetailProvider(album.id));
|
||||||
},
|
},
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -232,6 +239,19 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onActivitiesPressed(Album album) {
|
||||||
|
if (album.remoteId != null) {
|
||||||
|
context.autoPush(
|
||||||
|
ActivitiesRoute(
|
||||||
|
albumId: album.remoteId!,
|
||||||
|
appBarTitle: album.name,
|
||||||
|
isOwner: userId == album.ownerId,
|
||||||
|
isReadOnly: !album.activityEnabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: album.when(
|
appBar: album.when(
|
||||||
data: (data) => AlbumViewerAppbar(
|
data: (data) => AlbumViewerAppbar(
|
||||||
@@ -242,6 +262,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
selectionDisabled: disableSelection,
|
selectionDisabled: disableSelection,
|
||||||
onAddPhotos: onAddPhotosPressed,
|
onAddPhotos: onAddPhotosPressed,
|
||||||
onAddUsers: onAddUsersPressed,
|
onAddUsers: onAddUsersPressed,
|
||||||
|
onActivities: onActivitiesPressed,
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
error: (error, stackTrace) => AppBar(title: const Text("Error")),
|
||||||
loading: () => AppBar(),
|
loading: () => AppBar(),
|
||||||
@@ -266,6 +287,8 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
isOwner: userId == data.ownerId,
|
isOwner: userId == data.ownerId,
|
||||||
|
sharedAlbumId:
|
||||||
|
data.shared && data.activityEnabled ? data.remoteId : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||||
@@ -78,7 +79,7 @@ class AssetSelectionPage extends HookConsumerWidget {
|
|||||||
canDeselect ? "share_done" : "share_add",
|
canDeselect ? "share_done" : "share_add",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
@@ -34,11 +34,11 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
final selectedAssets = useState<Set<Asset>>(
|
final selectedAssets = useState<Set<Asset>>(
|
||||||
initialAssets != null ? Set.from(initialAssets!) : const {},
|
initialAssets != null ? Set.from(initialAssets!) : const {},
|
||||||
);
|
);
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
showSelectUserPage() async {
|
showSelectUserPage() async {
|
||||||
final bool? ok = await AutoRouter.of(context)
|
final bool? ok = await context.autoPush<bool?>(
|
||||||
.push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value));
|
SelectUserForSharingRoute(assets: selectedAssets.value),
|
||||||
|
);
|
||||||
if (ok == true) {
|
if (ok == true) {
|
||||||
selectedAssets.value = {};
|
selectedAssets.value = {};
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
onSelectPhotosButtonPressed() async {
|
onSelectPhotosButtonPressed() async {
|
||||||
AssetSelectionPageResult? selectedAsset =
|
AssetSelectionPageResult? selectedAsset =
|
||||||
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
|
await context.autoPush<AssetSelectionPageResult?>(
|
||||||
AssetSelectionRoute(
|
AssetSelectionRoute(
|
||||||
existingAssets: selectedAssets.value,
|
existingAssets: selectedAssets.value,
|
||||||
canDeselect: true,
|
canDeselect: true,
|
||||||
@@ -94,10 +94,10 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 200, left: 18),
|
padding: const EdgeInsets.only(top: 200, left: 18),
|
||||||
child: Text(
|
child: Text(
|
||||||
'create_shared_album_page_share_add_assets',
|
'create_shared_album_page_share_add_assets',
|
||||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
style: context.textTheme.displayMedium?.copyWith(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -117,7 +117,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isDarkTheme
|
color: context.isDarkTheme
|
||||||
? const Color.fromARGB(255, 63, 63, 63)
|
? const Color.fromARGB(255, 63, 63, 63)
|
||||||
: const Color.fromARGB(255, 206, 206, 206),
|
: const Color.fromARGB(255, 206, 206, 206),
|
||||||
),
|
),
|
||||||
@@ -128,16 +128,16 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
onPressed: onSelectPhotosButtonPressed,
|
onPressed: onSelectPhotosButtonPressed,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.add_rounded,
|
Icons.add_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
label: Padding(
|
label: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'create_shared_album_page_share_select_photos',
|
'create_shared_album_page_share_select_photos',
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -206,7 +206,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
selectedAssets.value = {};
|
selectedAssets.value = {};
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
|
|
||||||
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
|
context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,19 +214,19 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectedAssets.value = {};
|
selectedAssets.value = {};
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'share_create_album',
|
'share_create_album',
|
||||||
style: Theme.of(context).textTheme.displayMedium?.copyWith(
|
style: context.textTheme.displayMedium?.copyWith(
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
actions: [
|
actions: [
|
||||||
if (isSharedAlbum)
|
if (isSharedAlbum)
|
||||||
@@ -239,8 +239,8 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: albumTitleController.text.isEmpty
|
color: albumTitleController.text.isEmpty
|
||||||
? Theme.of(context).disabledColor
|
? context.themeData.disabledColor
|
||||||
: Theme.of(context).primaryColor,
|
: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -254,7 +254,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
'create_shared_album_page_create'.tr(),
|
'create_shared_album_page_create'.tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -265,7 +265,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/providers/album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -21,7 +21,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
final trashEnabled =
|
final trashEnabled =
|
||||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||||
final albums = ref.watch(albumProvider);
|
final albums = ref.watch(albumProvider);
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
var isDarkTheme = context.isDarkTheme;
|
||||||
var settings = ref.watch(appSettingsServiceProvider);
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -96,15 +96,14 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(right: 12.0),
|
padding: const EdgeInsets.only(right: 12.0),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
color: selected
|
color:
|
||||||
? Theme.of(context).primaryColor
|
selected ? context.primaryColor : Colors.transparent,
|
||||||
: Colors.transparent,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
option,
|
option,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selected ? Theme.of(context).primaryColor : null,
|
color: selected ? context.primaryColor : null,
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -122,13 +121,13 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.swap_vert_rounded,
|
Icons.swap_vert_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
options[selectedAlbumSortOrder.value],
|
options[selectedAlbumSortOrder.value],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -140,7 +139,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
Widget buildCreateAlbumButton() {
|
Widget buildCreateAlbumButton() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
|
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 32),
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
@@ -152,18 +151,18 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isDarkMode
|
color: isDarkTheme
|
||||||
? const Color.fromARGB(255, 53, 53, 53)
|
? const Color.fromARGB(255, 53, 53, 53)
|
||||||
: const Color.fromARGB(255, 203, 203, 203),
|
: const Color.fromARGB(255, 203, 203, 203),
|
||||||
),
|
),
|
||||||
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
|
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.add_rounded,
|
Icons.add_rounded,
|
||||||
size: 28,
|
size: 28,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -201,21 +200,21 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 13.0,
|
fontSize: 13.0,
|
||||||
color: isDarkMode ? Colors.white : Colors.grey[800],
|
color: isDarkTheme ? Colors.white : Colors.grey[800],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
|
backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
|
color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!,
|
||||||
),
|
),
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
),
|
),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
icon,
|
icon,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -228,7 +227,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
Widget? shareTrashButton() {
|
Widget? shareTrashButton() {
|
||||||
return trashEnabled
|
return trashEnabled
|
||||||
? InkWell(
|
? InkWell(
|
||||||
onTap: () => AutoRouter.of(context).push(const TrashRoute()),
|
onTap: () => context.autoPush(const TrashRoute()),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.delete_rounded,
|
Icons.delete_rounded,
|
||||||
@@ -257,12 +256,12 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
buildLibraryNavButton(
|
buildLibraryNavButton(
|
||||||
"library_page_favorites".tr(), Icons.favorite_border, () {
|
"library_page_favorites".tr(), Icons.favorite_border, () {
|
||||||
AutoRouter.of(context).navigate(const FavoritesRoute());
|
context.autoNavigate(const FavoritesRoute());
|
||||||
}),
|
}),
|
||||||
const SizedBox(width: 12.0),
|
const SizedBox(width: 12.0),
|
||||||
buildLibraryNavButton(
|
buildLibraryNavButton(
|
||||||
"library_page_archive".tr(), Icons.archive_outlined, () {
|
"library_page_archive".tr(), Icons.archive_outlined, () {
|
||||||
AutoRouter.of(context).navigate(const ArchiveRoute());
|
context.autoNavigate(const ArchiveRoute());
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -306,7 +305,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return AlbumThumbnailCard(
|
return AlbumThumbnailCard(
|
||||||
album: sorted[index - 1],
|
album: sorted[index - 1],
|
||||||
onTap: () => AutoRouter.of(context).push(
|
onTap: () => context.autoPush(
|
||||||
AlbumViewerRoute(
|
AlbumViewerRoute(
|
||||||
albumId: sorted[index - 1].id,
|
albumId: sorted[index - 1].id,
|
||||||
),
|
),
|
||||||
@@ -348,7 +347,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
childCount: local.length,
|
childCount: local.length,
|
||||||
(context, index) => AlbumThumbnailCard(
|
(context, index) => AlbumThumbnailCard(
|
||||||
album: local[index],
|
album: local[index],
|
||||||
onTap: () => AutoRouter.of(context).push(
|
onTap: () => context.autoPush(
|
||||||
AlbumViewerRoute(
|
AlbumViewerRoute(
|
||||||
albumId: local[index].id,
|
albumId: local[index].id,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/providers/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/album.dart';
|
import 'package:immich_mobile/shared/models/album.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.dart';
|
import 'package:immich_mobile/shared/models/user.dart';
|
||||||
@@ -22,14 +22,13 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
final sharedUsersList = useState<Set<User>>({});
|
final sharedUsersList = useState<Set<User>>({});
|
||||||
|
|
||||||
addNewUsersHandler() {
|
addNewUsersHandler() {
|
||||||
AutoRouter.of(context)
|
context.autoPop(sharedUsersList.value.map((e) => e.id).toList());
|
||||||
.pop(sharedUsersList.value.map((e) => e.id).toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTileIcon(User user) {
|
buildTileIcon(User user) {
|
||||||
if (sharedUsersList.value.contains(user)) {
|
if (sharedUsersList.value.contains(user)) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: context.primaryColor,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.check_rounded,
|
Icons.check_rounded,
|
||||||
size: 25,
|
size: 25,
|
||||||
@@ -50,7 +49,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: Chip(
|
child: Chip(
|
||||||
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
|
backgroundColor: context.primaryColor.withOpacity(0.15),
|
||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -124,7 +123,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop(null);
|
context.autoPop(null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/providers/album_title.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
|
||||||
@@ -35,9 +35,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||||
AutoRouter.of(context).pop(true);
|
context.autoPop(true);
|
||||||
AutoRouter.of(context)
|
context
|
||||||
.navigate(const TabControllerRoute(children: [SharingRoute()]));
|
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldMessenger(
|
ScaffoldMessenger(
|
||||||
@@ -50,7 +50,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
buildTileIcon(User user) {
|
buildTileIcon(User user) {
|
||||||
if (sharedUsersList.value.contains(user)) {
|
if (sharedUsersList.value.contains(user)) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: context.primaryColor,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.check_rounded,
|
Icons.check_rounded,
|
||||||
size: 25,
|
size: 25,
|
||||||
@@ -71,7 +71,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: Chip(
|
child: Chip(
|
||||||
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
|
backgroundColor: context.primaryColor.withOpacity(0.15),
|
||||||
label: Text(
|
label: Text(
|
||||||
user.email,
|
user.email,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -139,20 +139,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
'share_invite',
|
'share_invite',
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
style: TextStyle(color: context.primaryColor),
|
||||||
).tr(),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: Theme.of(context).primaryColor,
|
foregroundColor: context.primaryColor,
|
||||||
),
|
),
|
||||||
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
|
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
|
||||||
child: const Text(
|
child: const Text(
|
||||||
@@ -160,7 +160,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
// color: Theme.of(context).primaryColor,
|
// color: context.primaryColor,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/album/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
|
||||||
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
|
||||||
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
||||||
@@ -21,7 +21,6 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
|
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
|
||||||
final userId = ref.watch(currentUserProvider)?.id;
|
final userId = ref.watch(currentUserProvider)?.id;
|
||||||
final partner = ref.watch(partnerSharedWithProvider);
|
final partner = ref.watch(partnerSharedWithProvider);
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@@ -47,8 +46,9 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
album: sharedAlbums[index],
|
album: sharedAlbums[index],
|
||||||
showOwner: true,
|
showOwner: true,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context)
|
context.autoPush(
|
||||||
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
|
AlbumViewerRoute(albumId: sharedAlbums[index].id),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -79,12 +79,11 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
album.name,
|
album.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isDarkMode
|
color:
|
||||||
? Theme.of(context).primaryColor
|
context.isDarkTheme ? context.primaryColor : Colors.black,
|
||||||
: Colors.black,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
subtitle: isOwner
|
subtitle: isOwner
|
||||||
? Text(
|
? Text(
|
||||||
@@ -103,8 +102,9 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context)
|
context.autoPush(
|
||||||
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
|
AlbumViewerRoute(albumId: sharedAlbums[index].id),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -127,8 +127,7 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context)
|
context.autoPush(CreateAlbumRoute(isSharedAlbum: true));
|
||||||
.push(CreateAlbumRoute(isSharedAlbum: true));
|
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.photo_album_outlined,
|
Icons.photo_album_outlined,
|
||||||
@@ -147,8 +146,7 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 12.0),
|
const SizedBox(width: 12.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () =>
|
onPressed: () => context.autoPush(const SharedLinkRoute()),
|
||||||
AutoRouter.of(context).push(const SharedLinkRoute()),
|
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.link,
|
Icons.link,
|
||||||
size: 20,
|
size: 20,
|
||||||
@@ -191,21 +189,21 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.insert_photo_rounded,
|
Icons.insert_photo_rounded,
|
||||||
size: 50,
|
size: 50,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'sharing_page_empty_list',
|
'sharing_page_empty_list',
|
||||||
style: Theme.of(context).textTheme.displaySmall,
|
style: context.textTheme.displaySmall,
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'sharing_page_description',
|
'sharing_page_description',
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium,
|
||||||
).tr(),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -218,7 +216,7 @@ class SharingPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget sharePartnerButton() {
|
Widget sharePartnerButton() {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => AutoRouter.of(context).push(const PartnerRoute()),
|
onTap: () => context.autoPush(const PartnerRoute()),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.swap_horizontal_circle_rounded,
|
Icons.swap_horizontal_circle_rounded,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/archive/providers/archive_asset_provider.dart';
|
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@@ -30,7 +30,7 @@ class ArchivePage extends HookConsumerWidget {
|
|||||||
AppBar buildAppBar(String count) {
|
AppBar buildAppBar(String count) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => AutoRouter.of(context).pop(),
|
onPressed: () => context.autoPop(),
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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/modules/album/services/album.service.dart';
|
import 'package:immich_mobile/modules/album/services/album.service.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
|
||||||
@@ -67,7 +68,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Navigator.of(buildContext).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return const ShareDialog();
|
return const ShareDialog();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.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/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
|
||||||
class AdvancedBottomSheet extends HookConsumerWidget {
|
class AdvancedBottomSheet extends HookConsumerWidget {
|
||||||
@@ -11,8 +12,6 @@ class AdvancedBottomSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
@@ -40,7 +39,9 @@ class AdvancedBottomSheet extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 32.0),
|
const SizedBox(height: 32.0),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDarkMode ? Colors.grey[900] : Colors.grey[200],
|
color: context.isDarkTheme
|
||||||
|
? Colors.grey[900]
|
||||||
|
: Colors.grey[200],
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -70,7 +71,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.copy,
|
Icons.copy,
|
||||||
size: 16.0,
|
size: 16.0,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,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';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/asset_viewer/providers/asset_description.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
@@ -19,8 +20,7 @@ class DescriptionInput extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||||
final textColor = isDarkTheme ? Colors.white : Colors.black;
|
|
||||||
final controller = useTextEditingController();
|
final controller = useTextEditingController();
|
||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
final isFocus = useState(false);
|
final isFocus = useState(false);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.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:timezone/timezone.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
|
||||||
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@@ -26,12 +28,42 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
exifInfo.latitude != 0 &&
|
exifInfo.latitude != 0 &&
|
||||||
exifInfo.longitude != 0;
|
exifInfo.longitude != 0;
|
||||||
|
|
||||||
String get formattedDateTime {
|
String formatTimeZone(Duration d) =>
|
||||||
final fileCreatedAt = asset.fileCreatedAt.toLocal();
|
"GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
|
||||||
final date = DateFormat.yMMMEd().format(fileCreatedAt);
|
|
||||||
final time = DateFormat.jm().format(fileCreatedAt);
|
|
||||||
|
|
||||||
return '$date • $time';
|
String get formattedDateTime {
|
||||||
|
DateTime dt = asset.fileCreatedAt.toLocal();
|
||||||
|
String? timeZone;
|
||||||
|
if (asset.exifInfo?.dateTimeOriginal != null) {
|
||||||
|
dt = asset.exifInfo!.dateTimeOriginal!;
|
||||||
|
if (asset.exifInfo?.timeZone != null) {
|
||||||
|
dt = dt.toUtc();
|
||||||
|
try {
|
||||||
|
final location = getLocation(asset.exifInfo!.timeZone!);
|
||||||
|
dt = TZDateTime.from(dt, location);
|
||||||
|
} on LocationNotFoundException {
|
||||||
|
RegExp re = RegExp(
|
||||||
|
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
final m = re.firstMatch(asset.exifInfo!.timeZone!);
|
||||||
|
if (m != null) {
|
||||||
|
final duration = Duration(
|
||||||
|
hours: int.parse(m.group(1) ?? '0'),
|
||||||
|
minutes: int.parse(m.group(2) ?? '0'),
|
||||||
|
);
|
||||||
|
dt = dt.add(duration);
|
||||||
|
timeZone = formatTimeZone(duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final date = DateFormat.yMMMEd().format(dt);
|
||||||
|
final time = DateFormat.jm().format(dt);
|
||||||
|
timeZone ??= formatTimeZone(dt.timeZoneOffset);
|
||||||
|
|
||||||
|
return '$date • $time $timeZone';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
|
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
|
||||||
@@ -80,8 +112,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||||
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
|
||||||
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
|
||||||
var textColor = isDarkTheme ? Colors.white : Colors.black;
|
|
||||||
|
|
||||||
buildMap() {
|
buildMap() {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -297,9 +328,14 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null ? Text(
|
subtitle: exifInfo.f != null ||
|
||||||
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
|
exifInfo.exposureSeconds != null ||
|
||||||
) : null,
|
exifInfo.mm != null ||
|
||||||
|
exifInfo.iso != null
|
||||||
|
? Text(
|
||||||
|
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -368,7 +404,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
|||||||
data: (data) => DescriptionInput(asset: data),
|
data: (data) => DescriptionInput(asset: data),
|
||||||
error: (error, stackTrace) => Icon(
|
error: (error, stackTrace) => Icon(
|
||||||
Icons.image_not_supported_outlined,
|
Icons.image_not_supported_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
loading: () => const SizedBox(
|
loading: () => const SizedBox(
|
||||||
width: 75,
|
width: 75,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.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/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
required this.onFavorite,
|
required this.onFavorite,
|
||||||
required this.onUploadPressed,
|
required this.onUploadPressed,
|
||||||
required this.isOwner,
|
required this.isOwner,
|
||||||
|
required this.shareAlbumId,
|
||||||
|
required this.onActivitiesPressed,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
@@ -24,14 +27,23 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
final VoidCallback? onDownloadPressed;
|
final VoidCallback? onDownloadPressed;
|
||||||
final VoidCallback onToggleMotionVideo;
|
final VoidCallback onToggleMotionVideo;
|
||||||
final VoidCallback onAddToAlbumPressed;
|
final VoidCallback onAddToAlbumPressed;
|
||||||
|
final VoidCallback onActivitiesPressed;
|
||||||
final Function(Asset) onFavorite;
|
final Function(Asset) onFavorite;
|
||||||
final bool isPlayingMotionVideo;
|
final bool isPlayingMotionVideo;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
|
final String? shareAlbumId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
const double iconSize = 22.0;
|
const double iconSize = 22.0;
|
||||||
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
||||||
|
final comments = shareAlbumId != null
|
||||||
|
? ref.watch(
|
||||||
|
activityStatisticsStateProvider(
|
||||||
|
(albumId: shareAlbumId!, assetId: asset.remoteId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
Widget buildFavoriteButton(a) {
|
Widget buildFavoriteButton(a) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
@@ -94,6 +106,34 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildActivitiesButton() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
onActivitiesPressed();
|
||||||
|
},
|
||||||
|
icon: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.mode_comment_outlined,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
if (comments != 0)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Text(
|
||||||
|
comments.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildUploadButton() {
|
Widget buildUploadButton() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: onUploadPressed,
|
onPressed: onUploadPressed,
|
||||||
@@ -107,7 +147,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
Widget buildBackButton() {
|
Widget buildBackButton() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.arrow_back_ios_new_rounded,
|
Icons.arrow_back_ios_new_rounded,
|
||||||
@@ -130,6 +170,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||||
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
||||||
if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
|
if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
|
||||||
|
if (shareAlbumId != null) buildActivitiesButton(),
|
||||||
buildMoreInfoButton(),
|
buildMoreInfoButton(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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/modules/asset_viewer/providers/asset_stack.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
||||||
@@ -49,6 +50,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
final int heroOffset;
|
final int heroOffset;
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
|
final String? sharedAlbumId;
|
||||||
|
|
||||||
GalleryViewerPage({
|
GalleryViewerPage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -58,6 +60,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
this.heroOffset = 0,
|
this.heroOffset = 0,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
this.isOwner = true,
|
||||||
|
this.sharedAlbumId,
|
||||||
}) : controller = PageController(initialPage: initialIndex);
|
}) : controller = PageController(initialPage: initialIndex);
|
||||||
|
|
||||||
final PageController controller;
|
final PageController controller;
|
||||||
@@ -207,7 +210,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
if (isDeleted && isParent) {
|
if (isDeleted && isParent) {
|
||||||
if (totalAssets == 1) {
|
if (totalAssets == 1) {
|
||||||
// Handle only one asset
|
// Handle only one asset
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
} else {
|
} else {
|
||||||
// Go to next page otherwise
|
// Go to next page otherwise
|
||||||
controller.nextPage(
|
controller.nextPage(
|
||||||
@@ -291,7 +294,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final ratio = d.dy / max(d.dx.abs(), 1);
|
final ratio = d.dy / max(d.dx.abs(), 1);
|
||||||
if (d.dy > sensitivity && ratio > ratioThreshold) {
|
if (d.dy > sensitivity && ratio > ratioThreshold) {
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
} else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
|
} else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
|
||||||
showInfo();
|
showInfo();
|
||||||
}
|
}
|
||||||
@@ -306,7 +309,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
.watch(assetProvider.notifier)
|
.watch(assetProvider.notifier)
|
||||||
.toggleArchive([asset], !asset.isArchived);
|
.toggleArchive([asset], !asset.isArchived);
|
||||||
if (isParent) {
|
if (isParent) {
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeAssetFromStack();
|
removeAssetFromStack();
|
||||||
@@ -327,6 +330,19 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleActivities() {
|
||||||
|
if (sharedAlbumId != null) {
|
||||||
|
context.autoPush(
|
||||||
|
ActivitiesRoute(
|
||||||
|
albumId: sharedAlbumId!,
|
||||||
|
assetId: asset().remoteId,
|
||||||
|
withAssetThumbs: false,
|
||||||
|
isOwner: isOwner,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildAppBar() {
|
buildAppBar() {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !ref.watch(showControlsProvider),
|
ignoring: !ref.watch(showControlsProvider),
|
||||||
@@ -355,6 +371,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
|
||||||
}),
|
}),
|
||||||
onAddToAlbumPressed: () => addToAlbum(asset()),
|
onAddToAlbumPressed: () => addToAlbum(asset()),
|
||||||
|
shareAlbumId: sharedAlbumId,
|
||||||
|
onActivitiesPressed: handleActivities,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -443,7 +461,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: index == stackIndex.value
|
border: (stackIndex.value == -1 && index == 0) ||
|
||||||
|
index == stackIndex.value
|
||||||
? Border.all(
|
? Border.all(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
width: 2,
|
width: 2,
|
||||||
@@ -496,7 +515,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
stackElements.elementAt(stackIndex.value),
|
stackElements.elementAt(stackIndex.value),
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"viewer_stack_use_as_main_asset",
|
"viewer_stack_use_as_main_asset",
|
||||||
@@ -523,7 +542,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
childrenToRemove: [currentAsset],
|
childrenToRemove: [currentAsset],
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
} else {
|
} else {
|
||||||
await ref.read(assetStackServiceProvider).updateStack(
|
await ref.read(assetStackServiceProvider).updateStack(
|
||||||
currentAsset,
|
currentAsset,
|
||||||
@@ -551,7 +570,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
childrenToRemove: stack,
|
childrenToRemove: stack,
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
AutoRouter.of(context).pop();
|
context.autoPop();
|
||||||
},
|
},
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"viewer_unstack",
|
"viewer_unstack",
|
||||||
@@ -811,8 +830,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
placeholder: Image(
|
placeholder: Image(
|
||||||
image: provider,
|
image: provider,
|
||||||
fit: BoxFit.fitWidth,
|
fit: BoxFit.fitWidth,
|
||||||
height: MediaQuery.of(context).size.height,
|
height: context.height,
|
||||||
width: MediaQuery.of(context).size.width,
|
width: context.width,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
),
|
),
|
||||||
onVideoEnded: () {
|
onVideoEnded: () {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
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:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
||||||
@@ -44,7 +45,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
error: (error, stackTrace) => Icon(
|
error: (error, stackTrace) => Icon(
|
||||||
Icons.image_not_supported_outlined,
|
Icons.image_not_supported_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -74,8 +75,8 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: MediaQuery.of(context).size.height,
|
height: context.height,
|
||||||
width: MediaQuery.of(context).size.width,
|
width: context.width,
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
),
|
),
|
||||||
@@ -205,8 +206,8 @@ class _VideoPlayerState extends State<VideoPlayer> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height,
|
height: context.height,
|
||||||
width: MediaQuery.of(context).size.width,
|
width: context.width,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancellationToken(),
|
cancelToken: CancellationToken(),
|
||||||
autoBackup: Store.get(StoreKey.autoBackup, false),
|
autoBackup: Store.get(StoreKey.autoBackup, false),
|
||||||
backgroundBackup: false,
|
backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
|
||||||
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
|
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
|
||||||
backupRequireCharging:
|
backupRequireCharging:
|
||||||
Store.get(StoreKey.backupRequireCharging, false),
|
Store.get(StoreKey.backupRequireCharging, false),
|
||||||
@@ -171,6 +171,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
state.backupRequireCharging,
|
state.backupRequireCharging,
|
||||||
);
|
);
|
||||||
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
|
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
|
||||||
|
await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
|
||||||
} else {
|
} else {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backgroundBackup: wasEnabled,
|
backgroundBackup: wasEnabled,
|
||||||
@@ -383,6 +384,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
||||||
|
|
||||||
state = state.copyWith(backgroundBackup: isEnabled);
|
state = state.copyWith(backgroundBackup: isEnabled);
|
||||||
|
if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
|
||||||
|
Store.put(StoreKey.backgroundBackup, isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||||
await _getBackupAlbumsInfo();
|
await _getBackupAlbumsInfo();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -22,10 +22,10 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||||
final bool isExcluded =
|
final bool isExcluded =
|
||||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
|
|
||||||
ColorFilter selectedFilter = ColorFilter.mode(
|
ColorFilter selectedFilter = ColorFilter.mode(
|
||||||
Theme.of(context).primaryColor.withAlpha(100),
|
context.primaryColor.withAlpha(100),
|
||||||
BlendMode.darken,
|
BlendMode.darken,
|
||||||
);
|
);
|
||||||
ColorFilter excludedFilter =
|
ColorFilter excludedFilter =
|
||||||
@@ -46,7 +46,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: context.primaryColor,
|
||||||
);
|
);
|
||||||
} else if (isExcluded) {
|
} else if (isExcluded) {
|
||||||
return Chip(
|
return Chip(
|
||||||
@@ -194,7 +194,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
albumInfo.name,
|
albumInfo.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -224,13 +224,13 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(
|
context.autoPush(
|
||||||
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -25,14 +25,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||||||
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
|
|
||||||
ColorFilter selectedFilter = ColorFilter.mode(
|
ColorFilter selectedFilter = ColorFilter.mode(
|
||||||
Theme.of(context).primaryColor.withAlpha(100),
|
context.primaryColor.withAlpha(100),
|
||||||
BlendMode.darken,
|
BlendMode.darken,
|
||||||
);
|
);
|
||||||
ColorFilter excludedFilter =
|
ColorFilter excludedFilter =
|
||||||
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||||
ColorFilter unselectedFilter =
|
ColorFilter unselectedFilter =
|
||||||
const ColorFilter.mode(Colors.black, BlendMode.color);
|
const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||||
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
var assetCount = useState(0);
|
var assetCount = useState(0);
|
||||||
|
|
||||||
@@ -56,11 +55,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
buildTileColor() {
|
buildTileColor() {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return isDarkTheme
|
return context.isDarkTheme
|
||||||
? Theme.of(context).primaryColor.withAlpha(100)
|
? context.primaryColor.withAlpha(100)
|
||||||
: Theme.of(context).primaryColor.withAlpha(25);
|
: context.primaryColor.withAlpha(25);
|
||||||
} else if (isExcluded) {
|
} else if (isExcluded) {
|
||||||
return isDarkTheme
|
return context.isDarkTheme
|
||||||
? Colors.red[300]?.withAlpha(150)
|
? Colors.red[300]?.withAlpha(150)
|
||||||
: Colors.red[100]?.withAlpha(150);
|
: Colors.red[100]?.withAlpha(150);
|
||||||
} else {
|
} else {
|
||||||
@@ -159,13 +158,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||||||
subtitle: Text(assetCount.value.toString()),
|
subtitle: Text(assetCount.value.toString()),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(
|
context.autoPush(
|
||||||
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
AlbumPreviewRoute(album: albumInfo.albumEntity),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class BackupInfoCard extends StatelessWidget {
|
class BackupInfoCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
@@ -14,13 +15,11 @@ class BackupInfoCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20), // if you need this
|
borderRadius: BorderRadius.circular(20), // if you need this
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isDarkMode
|
color: context.isDarkTheme
|
||||||
? const Color.fromARGB(255, 56, 56, 56)
|
? const Color.fromARGB(255, 56, 56, 56)
|
||||||
: Colors.black12,
|
: Colors.black12,
|
||||||
width: 1,
|
width: 1,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
@@ -53,7 +53,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(const FailedBackupStatusRoute());
|
context.autoPush(const FailedBackupStatusRoute());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
Widget buildAssetInfoTable() {
|
Widget buildAssetInfoTable() {
|
||||||
return Table(
|
return Table(
|
||||||
border: TableBorder.all(
|
border: TableBorder.all(
|
||||||
color: Theme.of(context).primaryColorLight,
|
color: context.themeData.primaryColorLight,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@@ -176,7 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
onTap: () => isShowThumbnail.value = true,
|
onTap: () => isShowThumbnail.value = true,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
size: 30,
|
size: 30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -206,7 +206,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
|
|||||||
minHeight: 10.0,
|
minHeight: 10.0,
|
||||||
value: uploadProgress / 100.0,
|
value: uploadProgress / 100.0,
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: Colors.grey,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
@@ -54,7 +55,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Icons.bug_report,
|
Icons.bug_report,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => AutoRouter.of(context).pop(),
|
onPressed: () => context.autoPop(),
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
|
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
|
import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
|
||||||
@@ -18,7 +18,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
// final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
// final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
||||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = context.isDarkTheme;
|
||||||
final allAlbums = ref.watch(backupProvider).availableAlbums;
|
final allAlbums = ref.watch(backupProvider).availableAlbums;
|
||||||
|
|
||||||
// Albums which are displayed to the user
|
// Albums which are displayed to the user
|
||||||
@@ -118,7 +118,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: context.primaryColor,
|
||||||
deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
|
deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
|
||||||
deleteIcon: const Icon(
|
deleteIcon: const Icon(
|
||||||
Icons.cancel_rounded,
|
Icons.cancel_rounded,
|
||||||
@@ -211,7 +211,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => AutoRouter.of(context).pop(),
|
onPressed: () => context.autoPop(),
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@@ -315,7 +315,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
"backup_album_selection_page_albums_tap",
|
"backup_album_selection_page_albums_tap",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
@@ -325,7 +325,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.info,
|
Icons.info,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// show the dialog
|
// show the dialog
|
||||||
@@ -342,7 +342,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/backup/background_service/background.service.dart';
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||||
@@ -49,7 +49,6 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
!hasExclusiveAccess
|
!hasExclusiveAccess
|
||||||
? false
|
? false
|
||||||
: true;
|
: true;
|
||||||
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
final checkInProgress = useState(false);
|
final checkInProgress = useState(false);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -151,7 +150,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Icons.warning_rounded,
|
Icons.warning_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Check for corrupt asset backups",
|
"Check for corrupt asset backups",
|
||||||
@@ -187,7 +186,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
leading: isAutoBackup
|
leading: isAutoBackup
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.cloud_done_rounded,
|
Icons.cloud_done_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
)
|
)
|
||||||
: const Icon(Icons.cloud_off_rounded),
|
: const Icon(Icons.cloud_off_rounded),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -266,7 +265,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
).tr(),
|
).tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -279,7 +278,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
||||||
final bool isWifiRequired = backupState.backupRequireWifi;
|
final bool isWifiRequired = backupState.backupRequireWifi;
|
||||||
final bool isChargingRequired = backupState.backupRequireCharging;
|
final bool isChargingRequired = backupState.backupRequireCharging;
|
||||||
final Color activeColor = Theme.of(context).primaryColor;
|
final Color activeColor = context.primaryColor;
|
||||||
|
|
||||||
String formatBackupDelaySliderValue(double v) {
|
String formatBackupDelaySliderValue(double v) {
|
||||||
if (v == 0.0) {
|
if (v == 0.0) {
|
||||||
@@ -410,7 +409,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
max: 3.0,
|
max: 3.0,
|
||||||
divisions: 3,
|
divisions: 3,
|
||||||
label: formatBackupDelaySliderValue(triggerDelay.value),
|
label: formatBackupDelaySliderValue(triggerDelay.value),
|
||||||
activeColor: Theme.of(context).primaryColor,
|
activeColor: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@@ -511,7 +510,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -523,7 +522,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
"backup_controller_page_none_selected".tr(),
|
"backup_controller_page_none_selected".tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -562,7 +561,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isDarkMode
|
color: context.isDarkTheme
|
||||||
? const Color.fromARGB(255, 56, 56, 56)
|
? const Color.fromARGB(255, 56, 56, 56)
|
||||||
: Colors.black12,
|
: Colors.black12,
|
||||||
width: 1,
|
width: 1,
|
||||||
@@ -592,7 +591,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
trailing: ElevatedButton(
|
trailing: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
context.autoPush(const BackupAlbumSelectionRoute());
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"backup_controller_page_select",
|
"backup_controller_page_select",
|
||||||
@@ -678,7 +677,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||||
AutoRouter.of(context).pop(true);
|
context.autoPop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
@@ -20,7 +20,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).pop(true);
|
context.autoPop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -114,7 +114,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/modules/favorite/providers/favorite_provider.dart';
|
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
@@ -28,7 +28,7 @@ class FavoritesPage extends HookConsumerWidget {
|
|||||||
AppBar buildAppBar() {
|
AppBar buildAppBar() {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => AutoRouter.of(context).pop(),
|
onPressed: () => context.autoPop(),
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class GroupDividerTitle extends ConsumerWidget {
|
class GroupDividerTitle extends ConsumerWidget {
|
||||||
const GroupDividerTitle({
|
const GroupDividerTitle({
|
||||||
@@ -51,7 +52,7 @@ class GroupDividerTitle extends ConsumerWidget {
|
|||||||
child: multiselectEnabled && selected
|
child: multiselectEnabled && selected
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.check_circle_rounded,
|
Icons.check_circle_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: context.primaryColor,
|
||||||
)
|
)
|
||||||
: const Icon(
|
: const Icon(
|
||||||
Icons.check_circle_outline_rounded,
|
Icons.check_circle_outline_rounded,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
final bool showDragScroll;
|
final bool showDragScroll;
|
||||||
final bool showStack;
|
final bool showStack;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
|
final String? sharedAlbumId;
|
||||||
|
|
||||||
const ImmichAssetGrid({
|
const ImmichAssetGrid({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -55,6 +56,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
this.showDragScroll = true,
|
this.showDragScroll = true,
|
||||||
this.showStack = false,
|
this.showStack = false,
|
||||||
this.isOwner = true,
|
this.isOwner = true,
|
||||||
|
this.sharedAlbumId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -120,6 +122,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
showDragScroll: showDragScroll,
|
showDragScroll: showDragScroll,
|
||||||
showStack: showStack,
|
showStack: showStack,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
|
sharedAlbumId: sharedAlbumId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||