Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Tran
b49d9660f6 reduce widget rebuild 2023-11-02 21:33:33 -05:00
483 changed files with 15814 additions and 14581 deletions

View File

@@ -38,7 +38,7 @@ jobs:
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
@@ -70,7 +70,7 @@ jobs:
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.4.0
uses: stumpylog/image-cleaner-action/untagged@v0.3.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"

View File

@@ -33,9 +33,6 @@ jobs:
- context: "nginx"
image: "immich-proxy"
platforms: "linux/amd64,linux/arm64"
- context: "server"
image: "immich-server"
platforms: "linux/arm64,linux/amd64"
steps:
- name: Checkout
@@ -110,3 +107,87 @@ jobs:
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"
image: "immich-server"
platforms: "linux/arm64,linux/amd64"
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 }}

View File

@@ -1,29 +1,35 @@
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
dev-down:
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
dev-update:
dev-new-update:
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:
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:
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:
docker compose -f ./docker/docker-compose.staging.yml pull
docker-compose -f ./docker/docker-compose.staging.yml pull
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
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:
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:
cd ./server && npm run api:generate

View File

@@ -2,7 +2,7 @@
<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://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a>
<br/>
<br/>

130
cli/package-lock.json generated
View File

@@ -1489,21 +1489,21 @@
}
},
"node_modules/@types/byte-size": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz",
"integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.0.tgz",
"integrity": "sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==",
"dev": true
},
"node_modules/@types/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz",
"integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==",
"dev": true
},
"node_modules/@types/cli-progress": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz",
"integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==",
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz",
"integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -1543,9 +1543,9 @@
}
},
"node_modules/@types/jest": {
"version": "29.5.8",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
"integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
"version": "29.5.5",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
"integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -1553,9 +1553,9 @@
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
"integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==",
"dev": true
},
"node_modules/@types/json-schema": {
@@ -1565,28 +1565,25 @@
"dev": true
},
"node_modules/@types/mime-types": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz",
"integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==",
"dev": true
},
"node_modules/@types/mock-fs": {
"version": "4.13.4",
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
"integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz",
"integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
"version": "20.8.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz",
"integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==",
"dev": true
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.2",
@@ -3870,9 +3867,9 @@
}
},
"node_modules/jest-extended": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz",
"integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz",
"integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==",
"dev": true,
"dependencies": {
"jest-diff": "^29.0.0",
@@ -5865,12 +5862,6 @@
"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": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -7279,21 +7270,21 @@
}
},
"@types/byte-size": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz",
"integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.0.tgz",
"integrity": "sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==",
"dev": true
},
"@types/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz",
"integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==",
"dev": true
},
"@types/cli-progress": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz",
"integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==",
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz",
"integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -7333,9 +7324,9 @@
}
},
"@types/jest": {
"version": "29.5.8",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz",
"integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==",
"version": "29.5.5",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
"integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
"dev": true,
"requires": {
"expect": "^29.0.0",
@@ -7343,9 +7334,9 @@
}
},
"@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
"integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==",
"dev": true
},
"@types/json-schema": {
@@ -7355,28 +7346,25 @@
"dev": true
},
"@types/mime-types": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz",
"integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==",
"dev": true
},
"@types/mock-fs": {
"version": "4.13.4",
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
"integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz",
"integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
}
"version": "20.8.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz",
"integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==",
"dev": true
},
"@types/normalize-package-data": {
"version": "2.4.2",
@@ -8981,9 +8969,9 @@
}
},
"jest-extended": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz",
"integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz",
"integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==",
"dev": true,
"requires": {
"jest-diff": "^29.0.0",
@@ -10431,12 +10419,6 @@
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"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": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.85.0
* The version of the OpenAPI document: 1.84.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.85.0
* The version of the OpenAPI document: 1.84.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.85.0
* The version of the OpenAPI document: 1.84.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.85.0
* The version of the OpenAPI document: 1.84.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -1,11 +1,5 @@
# See:
# - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting
version: "3.8"
name: immich-dev
services:
immich-server:
container_name: immich_server
@@ -77,6 +71,10 @@ services:
command: npm run dev --host
env_file:
- .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:
- 3000:3000
- 24678:24678
@@ -123,11 +121,11 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env
environment:

View File

@@ -1,7 +1,5 @@
version: "3.8"
name: immich-prod
services:
immich-server:
container_name: immich_server
@@ -9,9 +7,9 @@ services:
build:
context: ../server
dockerfile: Dockerfile
command: [ "./start-server.sh" ]
command: ["./start-server.sh"]
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
@@ -20,6 +18,19 @@ services:
- database
- 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:
container_name: immich_microservices
image: immich-microservices:latest
@@ -29,9 +40,9 @@ services:
build:
context: ../server
dockerfile: Dockerfile
command: [ "./start-microservices.sh" ]
command: ["./start-microservices.sh"]
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
@@ -53,18 +64,6 @@ services:
depends_on:
- 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:
container_name: immich_typesense
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
@@ -74,17 +73,17 @@ services:
# remove this to get debug messages
- GLOG_minloglevel=1
volumes:
- ${UPLOAD_LOCATION}/typesense:/data
- tsdata:/data
restart: always
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
restart: always
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env
environment:
@@ -92,7 +91,7 @@ services:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
- pgdata:/var/lib/postgresql/data
restart: always
immich-proxy:
@@ -114,4 +113,6 @@ services:
restart: always
volumes:
pgdata:
model-cache:
tsdata:

View File

@@ -23,7 +23,7 @@ services:
- database
database:
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
command: -c fsync=off
environment:
POSTGRES_PASSWORD: postgres

View File

@@ -1,12 +1,10 @@
version: "3.8"
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
command: ["start.sh", "immich"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
@@ -24,7 +22,7 @@ services:
# extends:
# file: hwaccel.yml
# service: hwaccel
command: [ "start.sh", "microservices" ]
command: ["start.sh", "microservices"]
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
@@ -66,12 +64,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:3995fe6ea6a619313e31046bd3c8643f9e70f8f2b294ff82659d409b47d06abb
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
restart: always
database:
container_name: immich_postgres
image: postgres:14-alpine@sha256:874f566dd512d79cf74f59754833e869ae76ece96716d153b0fa3e64aec88d92
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
env_file:
- .env
environment:

View File

@@ -17,13 +17,13 @@ docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/back
```
```bash title='Restore'
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 create # Create Docker containers for Immich apps without running them.
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 create # Create Docker containers for Immich apps without running them.
docker start immich_postgres # Start Postgres server
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
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.).

View File

@@ -51,7 +51,8 @@ immich-admin list-users
{
id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
email: 'immich@example.com.com',
name: 'Immich Admin',
firstName: 'Immich',
lastName: 'Admin',
storageLabel: 'admin',
externalPath: null,
profileImagePath: 'upload/profile/e65e6f88-2a30-4dbe-8dd9-1885f4889b53/e65e6f88-2a30-4dbe-8dd9-1885f4889b53.jpg',

View File

@@ -9,6 +9,6 @@ npm run typeorm:migrations:generate ./src/infra/<migration-name>
```
2. Check if the migration file makes sense.
3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
3. Move the migration file to folder `./src/infra/database/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.

View File

@@ -1,19 +0,0 @@
# 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`.

View File

@@ -4,10 +4,6 @@ You can use the CLI to upload an existing gallery to the Immich server
[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
- Node.js 16 or above

View File

@@ -1,7 +1,5 @@
# 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.
The list of people is shown in the Explore page.
@@ -15,16 +13,3 @@ 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.
<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%"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

View File

@@ -75,7 +75,7 @@ Some basic examples:
- `*.tif` will exclude all files with the extension `.tif`
- `hidden.jpg` will exclude all files named `hidden.jpg`
- `**/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

View File

@@ -1,6 +1,6 @@
import MobileAppDownload from '../partials/_mobile-app-download.md';
import MobileAppLogin from '../partials/_mobile-app-login.md';
import MobileAppBackup from '../partials/_mobile-app-backup.md';
import MobileAppBackup from '../partials/_mobile-app-login.md';
# Mobile App

View File

@@ -1,42 +0,0 @@
# 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')
```

View File

@@ -17,12 +17,6 @@ The default configuration looks like this:
"targetAudioCodec": "aac",
"targetResolution": "720",
"maxBitrate": "0",
"bframes": -1,
"refs": 0,
"gopSize": 0,
"npl": 0,
"temporalAQ": false,
"cqMode": "auto",
"twoPass": false,
"transcode": "required",
"tonemap": "hable",
@@ -50,15 +44,9 @@ The default configuration looks like this:
"sidecar": {
"concurrency": 5
},
"library": {
"concurrency": 5
},
"storageTemplateMigration": {
"concurrency": 5
},
"migration": {
"concurrency": 5
},
"thumbnailGeneration": {
"concurrency": 5
},
@@ -67,16 +55,16 @@ The default configuration looks like this:
}
},
"machineLearning": {
"classification": {
"minScore": 0.7,
"enabled": true,
"modelName": "microsoft/resnet-50"
},
"enabled": true,
"url": "http://immich-machine-learning:3003",
"classification": {
"enabled": true,
"modelName": "microsoft/resnet-50",
"minScore": 0.9
},
"clip": {
"enabled": true,
"modelName": "ViT-B-32__openai"
"modelName": "ViT-B-32::openai"
},
"facialRecognition": {
"enabled": true,
@@ -86,14 +74,6 @@ The default configuration looks like this:
"minFaces": 1
}
},
"map": {
"enabled": true,
"tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
},
"reverseGeocoding": {
"enabled": true,
"citiesFileOverride": "cities500"
},
"oauth": {
"enabled": false,
"issuerUrl": "",
@@ -116,27 +96,8 @@ The default configuration looks like this:
"thumbnail": {
"webpSize": 250,
"jpegSize": 1440,
"quality": 80,
"quality": 90,
"colorspace": "p3"
},
"newVersionCheck": {
"enabled": true
},
"trash": {
"enabled": true,
"days": 30
},
"theme": {
"customCss": ""
},
"library": {
"scan": {
"enabled": true,
"cronExpression": "0 0 * * *"
}
},
"stylesheets": {
"css": ""
}
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 335 KiB

20
docs/package-lock.json generated
View File

@@ -13232,19 +13232,19 @@
}
},
"node_modules/tailwindcss": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.0",
"fast-glob": "^3.2.12",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.19.1",
"jiti": "^1.18.2",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -24517,19 +24517,19 @@
}
},
"tailwindcss": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"requires": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.0",
"fast-glob": "^3.2.12",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.19.1",
"jiti": "^1.18.2",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",

View File

@@ -34,7 +34,7 @@ function HomepageHeader() {
</Link>
</div>
<img src="/img/immich-screenshots.png" alt="screenshots" width={'85%'} />
<img src="/img/immich-screenshots.png" alt="logo" />
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
<div className="h-24">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -38,16 +38,8 @@ class LogSettings(BaseSettings):
_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:
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)}"
return Path(settings.cache_folder) / model_type.value / model_name.translate(_clean_name)
LOG_LEVELS: dict[str, int] = {

View File

@@ -3,8 +3,7 @@ from typing import Any
from app.schemas import ModelType
from .base import InferenceModel
from .clip import MCLIPEncoder, OpenCLIPEncoder
from .constants import is_insightface, is_mclip, is_openclip
from .clip import MCLIPEncoder, OpenCLIPEncoder, is_mclip, is_openclip
from .facial_recognition import FaceRecognizer
from .image_classification import ImageClassifier
@@ -16,12 +15,11 @@ def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any)
return OpenCLIPEncoder(model_name, **model_kwargs)
elif is_mclip(model_name):
return MCLIPEncoder(model_name, **model_kwargs)
else:
raise ValueError(f"Unknown CLIP model {model_name}")
case ModelType.FACIAL_RECOGNITION:
if is_insightface(model_name):
return FaceRecognizer(model_name, **model_kwargs)
return FaceRecognizer(model_name, **model_kwargs)
case ModelType.IMAGE_CLASSIFICATION:
return ImageClassifier(model_name, **model_kwargs)
case _:
raise ValueError(f"Unknown model type {model_type}")
raise ValueError(f"Unknown ${model_type} model {model_name}")

View File

@@ -7,9 +7,8 @@ from shutil import rmtree
from typing import Any
import onnxruntime as ort
from huggingface_hub import snapshot_download
from ..config import get_cache_dir, get_hf_model_name, log, settings
from ..config import get_cache_dir, log, settings
from ..schemas import ModelType
@@ -79,13 +78,9 @@ class InferenceModel(ABC):
def configure(self, **model_kwargs: Any) -> None:
pass
@abstractmethod
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
def _load(self) -> None:

View File

@@ -7,10 +7,11 @@ from typing import Any, Literal
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
from PIL import Image
from transformers import AutoTokenizer
from app.config import clean_name, log
from app.config import log
from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy
from app.schemas import ModelType, ndarray_f32, ndarray_i32, ndarray_i64
@@ -116,7 +117,15 @@ class OpenCLIPEncoder(BaseCLIPEncoder):
mode: Literal["text", "vision"] | None = None,
**model_kwargs: Any,
) -> None:
super().__init__(clean_name(model_name), cache_dir, mode, **model_kwargs)
super().__init__(_clean_model_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:
super()._load()
@@ -162,3 +171,52 @@ class MCLIPEncoder(OpenCLIPEncoder):
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
tokens: dict[str, ndarray_i64] = self.tokenizer(text, return_tensors="np")
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

View File

@@ -1,57 +0,0 @@
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

View File

@@ -1,3 +1,4 @@
import zipfile
from pathlib import Path
from typing import Any
@@ -6,8 +7,8 @@ import numpy as np
import onnxruntime as ort
from insightface.model_zoo import ArcFaceONNX, RetinaFace
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 .base import InferenceModel
@@ -24,21 +25,37 @@ class FaceRecognizer(InferenceModel):
**model_kwargs: Any,
) -> None:
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
super().__init__(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:
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(
session=ort.InferenceSession(
self.det_file.as_posix(),
det_file.as_posix(),
sess_options=self.sess_options,
providers=self.providers,
provider_options=self.provider_options,
),
)
self.rec_model = ArcFaceONNX(
self.rec_file.as_posix(),
rec_file.as_posix(),
session=ort.InferenceSession(
self.rec_file.as_posix(),
rec_file.as_posix(),
sess_options=self.sess_options,
providers=self.providers,
provider_options=self.provider_options,
@@ -86,15 +103,7 @@ class FaceRecognizer(InferenceModel):
@property
def cached(self) -> bool:
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"
return self.cache_dir.is_dir() and any(self.cache_dir.glob("*.onnx"))
def configure(self, **model_kwargs: Any) -> None:
self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)

View File

@@ -106,13 +106,13 @@ class TestCLIP:
class TestFaceRecognition:
def test_set_min_score(self, mocker: MockerFixture) -> None:
mocker.patch.object(FaceRecognizer, "load")
face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache", min_score=0.5)
face_recognizer = FaceRecognizer("test_model_name", cache_dir="test_cache", min_score=0.5)
assert face_recognizer.min_score == 0.5
def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
mocker.patch.object(FaceRecognizer, "load")
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
face_recognizer = FaceRecognizer("test_model_name", min_score=0.0, cache_dir="test_cache")
det_model = mock.Mock()
num_faces = 2

View File

@@ -14,7 +14,8 @@ RUN micromamba install -y -n base -f /tmp/conda-lock.yml && \
WORKDIR /usr/src/app
COPY --chown=$MAMBA_USER:$MAMBA_USER export .
COPY --chown=$MAMBA_USER:$MAMBA_USER start.sh .
COPY --chown=$MAMBA_USER:$MAMBA_USER app .
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
CMD ["python -m run"]
CMD ["./start.sh"]

View File

@@ -1,9 +0,0 @@
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"),
}

View File

@@ -1,15 +1,22 @@
import tempfile
import warnings
from pathlib import Path
from export.models.constants import MCLIP_TO_OPENCLIP
import torch
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
from transformers import AutoTokenizer
from .openclip import OpenCLIPModelConfig
from .openclip import to_onnx as openclip_to_onnx
from .optimize import optimize
from .util import get_model_path, clean_name
from .util import get_model_path
_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(
@@ -26,7 +33,7 @@ def to_onnx(
param.requires_grad_(False)
export_text_encoder(model, textual_path)
openclip_to_onnx(MCLIP_TO_OPENCLIP[clean_name(model_name)], output_dir_visual)
openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
optimize(textual_path)

View File

@@ -3,9 +3,6 @@ from pathlib import Path
from typing import Any
_clean_name = str.maketrans(":\\/", "___", ".")
def get_model_path(output_dir: Path | str) -> Path:
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
@@ -16,7 +13,3 @@ def save_config(config: Any, output_path: Path | str) -> None:
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
json.dump(config, output_path.open("w"))
def clean_name(model_name: str) -> str:
return model_name.split("/")[-1].translate(_clean_name)

View File

@@ -1,131 +1,76 @@
from enum import StrEnum
import gc
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Optional
from huggingface_hub import create_repo, upload_folder
from export.models import mclip, openclip, insightface
from export.models.util import clean_name
from huggingface_hub import create_repo, login, upload_folder
from models import mclip, openclip
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",
]
app = typer.Typer()
login(token=os.environ["HF_AUTH_TOKEN"])
with Progress() as progress:
task1 = progress.add_task("[green]Exporting models...", total=len(models))
task2 = progress.add_task("[yellow]Uploading models...", total=len(models))
class ModelLibrary(StrEnum):
MCLIP = "mclip"
OPENCLIP = "openclip"
INSIGHTFACE = "insightface"
with TemporaryDirectory() as tmp:
tmpdir = Path(tmp)
for model in models:
model_name = model.split("/")[-1].replace("::", "__")
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}"
def _export(model_name: str, library: ModelLibrary, export_dir: Path) -> None:
visual_dir = export_dir / "visual"
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)
create_repo(repo_id, exist_ok=True)
upload_folder(repo_id=repo_id, folder_path=tmpdir / model_name)
progress.update(task2, advance=1)
gc.collect()
def export() -> None:
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()
def _upload(repo_id: str, upload_dir: Path, auth_token: str | None = os.environ.get("HF_AUTH_TOKEN", None)) -> None:
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()
export()
upload()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.85.0"
version = "1.84.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 109,
"android.injected.version.name" => "1.85.0",
"android.injected.version.code" => 108,
"android.injected.version.name" => "1.84.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')

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000244">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000625">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.0562">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.943413">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.087498">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.374484">
</testcase>

View File

@@ -23,7 +23,6 @@
"album_viewer_appbar_share_err_title": "Failed to change album title",
"album_viewer_appbar_share_leave": "Leave album",
"album_viewer_appbar_share_remove": "Remove from album",
"album_viewer_appbar_share_to": "Share To",
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
@@ -114,7 +113,7 @@
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_title": "Caching Settings",
"change_password_form_confirm_password": "Confirm Password",
"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_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_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
@@ -316,7 +315,6 @@
"shared_link_edit_description": "Description",
"shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_password": "Password",
"shared_link_edit_expire_after": "Expire after",
"shared_link_edit_password_hint": "Enter the share password",
"shared_link_edit_show_meta": "Show metadata",
"shared_link_edit_submit_button": "Update link",
@@ -374,11 +372,5 @@
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"app_bar_signout_dialog_title": "Sign out",
"app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
"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"
"app_bar_signout_dialog_ok": "Yes"
}

View File

@@ -28,6 +28,12 @@ PODS:
- Flutter
- isar_flutter_libs (1.0.0):
- Flutter
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -42,6 +48,8 @@ PODS:
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- SAMKeychain (1.5.3)
- screen_brightness_ios (0.1.0):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -55,6 +63,8 @@ PODS:
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
@@ -71,16 +81,21 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
@@ -115,6 +130,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@@ -125,6 +146,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -135,6 +158,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
@@ -152,6 +177,9 @@ SPEC CHECKSUMS:
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
@@ -159,14 +187,16 @@ SPEC CHECKSUMS:
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.12.1
COCOAPODS: 1.11.3

View File

@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 125;
CURRENT_PROJECT_VERSION = 124;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 125;
CURRENT_PROJECT_VERSION = 124;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 125;
CURRENT_PROJECT_VERSION = 124;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -59,11 +59,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.85.0</string>
<string>1.84.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>125</string>
<string>124</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.85.0"
version_number: "1.84.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000291">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.199372">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.181977">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.104477">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="16.12614">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.164465">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.162663">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="108.828838">
<testcase classname="fastlane.lanes" name="4: build_app" time="145.399278">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="60.89387">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="61.317235">
</testcase>

View File

@@ -1,54 +0,0 @@
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);
}

View File

@@ -1,36 +0,0 @@
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';
}
}

View File

@@ -1,30 +0,0 @@
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);
}
}

View File

@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.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/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
@@ -36,6 +35,7 @@ import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:media_kit/media_kit.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
@@ -50,6 +50,7 @@ void main() async {
Future<void> initApp() async {
await EasyLocalization.ensureInitialized();
MediaKit.ensureInitialized();
if (kReleaseMode && Platform.isAndroid) {
try {
@@ -78,8 +79,6 @@ Future<void> initApp() async {
log.severe('Catch all error: ${error.toString()} - $error', error, stack);
return true;
};
initializeTimeZones();
}
Future<Isar> loadDb() async {

View File

@@ -1,89 +0,0 @@
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;
}
}

View File

@@ -1,130 +0,0 @@
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,
);
});

View File

@@ -1,85 +0,0 @@
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;
}
}

View File

@@ -1,317 +0,0 @@
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),
),
),
],
),
);
},
),
);
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.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/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -11,7 +10,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart';
class SharedAlbumNotifier extends StateNotifier<List<Album>> {
SharedAlbumNotifier(this._albumService, Isar db, this._ref) : super([]) {
SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
query.findAll().then((value) => state = value);
_streamSub = query.watch().listen((data) => state = data);
@@ -19,7 +18,6 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
final AlbumService _albumService;
late final StreamSubscription<List<Album>> _streamSub;
final Ref _ref;
Future<Album?> createSharedAlbum(
String albumName,
@@ -68,17 +66,6 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
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
void dispose() {
_streamSub.cancel();
@@ -91,6 +78,5 @@ final sharedAlbumProvider =
return SharedAlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(dbProvider),
ref,
);
});

View File

@@ -284,23 +284,6 @@ class AlbumService {
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 {
try {
final userId = Store.get(StoreKey.currentUser).isarId;

View File

@@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -95,19 +95,20 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
children: [
Text(
'common_add_to_album'.tr(),
style: context.textTheme.displayMedium,
style: Theme.of(context).textTheme.displayMedium,
),
TextButton.icon(
icon: Icon(
Icons.add,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
label: Text(
'common_create_new_album'.tr(),
style: TextStyle(color: context.primaryColor),
style:
TextStyle(color: Theme.of(context).primaryColor),
),
onPressed: () {
context.autoPush(
AutoRouter.of(context).push(
CreateAlbumRoute(
isSharedAlbum: false,
initialAssets: assets,

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class AlbumActionOutlinedButton extends StatelessWidget {
final VoidCallback? onPressed;
@@ -15,6 +14,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon(
@@ -25,7 +26,7 @@ class AlbumActionOutlinedButton extends StatelessWidget {
),
side: BorderSide(
width: 1,
color: context.isDarkTheme
color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
),
@@ -33,13 +34,13 @@ class AlbumActionOutlinedButton extends StatelessWidget {
icon: Icon(
iconData,
size: 15,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
label: Text(
labelText,
style: context.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
onPressed: onPressed,
),

View File

@@ -1,6 +1,5 @@
import 'package:easy_localization/easy_localization.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/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -23,8 +22,7 @@ class AlbumThumbnailCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
var isDarkTheme = context.isDarkTheme;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return LayoutBuilder(
builder: (context, constraints) {
var cardSize = constraints.maxWidth;
@@ -34,7 +32,7 @@ class AlbumThumbnailCard extends StatelessWidget {
height: cardSize,
width: cardSize,
decoration: BoxDecoration(
color: isDarkTheme ? Colors.grey[800] : Colors.grey[200],
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
child: Center(
child: Icon(
@@ -75,14 +73,14 @@ class AlbumThumbnailCard extends StatelessWidget {
style: TextStyle(
fontFamily: 'WorkSans',
fontSize: 12,
color: isDarkTheme ? Colors.white : Colors.black,
color: isDarkMode ? Colors.white : Colors.black,
),
),
if (owner != null) const TextSpan(text: ' · '),
if (owner != null)
TextSpan(
text: owner,
style: context.textTheme.labelSmall,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
@@ -116,8 +114,8 @@ class AlbumThumbnailCard extends StatelessWidget {
album.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isDarkTheme
? context.primaryColor
color: isDarkMode
? Theme.of(context).primaryColor
: Colors.black,
),
),

View File

@@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.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/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart';
@@ -21,11 +21,12 @@ class AlbumThumbnailListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
var cardSize = 68.0;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
buildEmptyThumbnail() {
return Container(
decoration: BoxDecoration(
color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200],
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
child: SizedBox(
height: cardSize,
@@ -60,7 +61,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onTap: onTap ??
() {
context.autoPush(AlbumViewerRoute(albumId: album.id));
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),

View File

@@ -1,7 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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';
class AlbumTitleTextField extends ConsumerWidget {
@@ -20,7 +19,7 @@ class AlbumTitleTextField extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = context.isDarkTheme;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return TextField(
onChanged: (v) {
@@ -56,7 +55,7 @@ class AlbumTitleTextField extends ConsumerWidget {
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
splashRadius: 10,
)

View File

@@ -1,15 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.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_detail.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/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/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -27,7 +24,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
required this.titleFocusNode,
this.onAddPhotos,
this.onAddUsers,
required this.onActivities,
}) : super(key: key);
final Album album;
@@ -37,19 +33,11 @@ class AlbumViewerAppbar extends HookConsumerWidget
final FocusNode titleFocusNode;
final Function(Album album)? onAddPhotos;
final Function(Album album)? onAddUsers;
final Function(Album album) onActivities;
@override
Widget build(BuildContext context, WidgetRef ref) {
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
final comments = album.shared
? ref.watch(
activityStatisticsStateProvider(
(albumId: album.remoteId!, assetId: null),
),
)
: 0;
deleteAlbum() async {
ImmichLoadingOverlayController.appLoader.show();
@@ -58,12 +46,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
if (album.shared) {
success =
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
context
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
context
.autoNavigate(const TabControllerRoute(children: [LibraryRoute()]));
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [LibraryRoute()]));
}
if (!success) {
ImmichToast.show(
@@ -93,7 +81,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
child: Text(
'Cancel',
style: TextStyle(
color: context.primaryColor,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
@@ -107,7 +95,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
'Confirm',
style: TextStyle(
fontWeight: FontWeight.bold,
color: !context.isDarkTheme ? Colors.red : Colors.red[300],
color: Theme.of(context).brightness == Brightness.light
? Colors.red
: Colors.red[300],
),
),
),
@@ -128,8 +118,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
if (isSuccess) {
context
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
Navigator.pop(context);
ImmichToast.show(
@@ -170,81 +160,40 @@ class AlbumViewerAppbar extends HookConsumerWidget
ImmichLoadingOverlayController.appLoader.hide();
}
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() {
buildBottomSheetActionButton() {
if (selected.isNotEmpty) {
return [
ListTile(
leading: const Icon(Icons.ios_share_rounded),
if (album.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_sweep_rounded),
title: const Text(
'album_viewer_appbar_share_to',
'album_viewer_appbar_share_remove',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => onShareAssetsTo(),
),
album.ownerId == userId
? 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(),
];
onTap: () => onRemoveFromAlbumPressed(),
);
} else {
return const SizedBox();
}
} else {
return [
album.ownerId == userId
? ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'album_viewer_appbar_share_delete',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => onDeleteAlbumPressed(),
)
: ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'album_viewer_appbar_share_leave',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => onLeaveAlbumPressed(),
),
];
if (album.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(
'album_viewer_appbar_share_delete',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => onDeleteAlbumPressed(),
);
} else {
return ListTile(
leading: const Icon(Icons.person_remove_rounded),
title: const Text(
'album_viewer_appbar_share_leave',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
onTap: () => onLeaveAlbumPressed(),
);
}
}
}
@@ -264,7 +213,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
ListTile(
leading: const Icon(Icons.share_rounded),
onTap: () {
context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
AutoRouter.of(context)
.push(SharedLinkEditRoute(albumId: album.remoteId));
Navigator.pop(context);
},
title: const Text(
@@ -274,7 +224,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
),
ListTile(
leading: const Icon(Icons.settings_rounded),
onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
onTap: () =>
AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)),
title: const Text(
"translated_text_options",
style: TextStyle(fontWeight: FontWeight.bold),
@@ -296,7 +247,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
),
];
showModalBottomSheet(
backgroundColor: context.scaffoldBackgroundColor,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false,
context: context,
builder: (context) {
@@ -306,7 +257,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...buildBottomSheetActions(),
buildBottomSheetActionButton(),
if (selected.isEmpty && onAddPhotos != null) ...commonActions,
if (selected.isEmpty &&
onAddPhotos != null &&
@@ -320,33 +271,6 @@ 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() {
if (selected.isNotEmpty) {
return IconButton(
@@ -377,7 +301,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
);
} else {
return IconButton(
onPressed: () async => await context.autoPop(),
onPressed: () async => await AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
splashRadius: 25,
);
@@ -390,8 +314,6 @@ class AlbumViewerAppbar extends HookConsumerWidget
title: selected.isNotEmpty ? Text('${selected.length}') : null,
centerTitle: false,
actions: [
if (album.shared && (album.activityEnabled || comments != 0))
buildActivitiesButton(),
if (album.isRemote)
IconButton(
splashRadius: 25,

View File

@@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/shared/models/album.dart';
@@ -18,6 +17,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final titleTextEditController = useTextEditingController(text: album.name);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -65,7 +65,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
},
icon: Icon(
Icons.cancel_rounded,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
splashRadius: 10,
)
@@ -79,14 +79,14 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
borderRadius: BorderRadius.circular(10),
),
focusColor: Colors.grey[300],
fillColor: context.isDarkTheme
fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: titleFocusNode.hasFocus,
hintText: 'share_add_title'.tr(),
hintStyle: TextStyle(
fontSize: 28,
color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.bold,
),
),

View File

@@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.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/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -23,7 +23,6 @@ class AlbumOptionsPage extends HookConsumerWidget {
final sharedUsers = useState(album.sharedUsers.toList());
final owner = album.owner.value;
final userId = ref.watch(authenticationProvider).userId;
final activityEnabled = useState(album.activityEnabled);
final isOwner = owner?.id == userId;
void showErrorMessage() {
@@ -44,9 +43,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
if (isSuccess) {
context.autoNavigate(
const TabControllerRoute(children: [SharingRoute()]),
);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
showErrorMessage();
}
@@ -98,7 +96,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
}
showModalBottomSheet(
backgroundColor: context.scaffoldBackgroundColor,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false,
context: context,
builder: (context) {
@@ -124,7 +122,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
)
: const SizedBox(),
title: Text(
album.owner.value?.name ?? "",
album.owner.value?.firstName ?? "",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
@@ -155,7 +153,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
radius: 22,
),
title: Text(
user.name,
user.firstName,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
@@ -178,7 +176,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
buildSectionTitle(String text) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text(text, style: context.textTheme.bodySmall),
child: Text(text, style: Theme.of(context).textTheme.bodySmall),
);
}
@@ -187,7 +185,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () {
context.autoPop(null);
AutoRouter.of(context).pop(null);
},
),
centerTitle: true,
@@ -197,29 +195,6 @@ class AlbumOptionsPage extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
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"),
buildOwnerInfo(),
buildSharedUsersList(),

View File

@@ -1,10 +1,10 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/providers/album_detail.provider.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.
void onAddPhotosPressed(Album albumInfo) async {
AssetSelectionPageResult? returnPayload =
await context.autoPush<AssetSelectionPageResult?>(
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: albumInfo.assets,
canDeselect: false,
@@ -97,7 +97,8 @@ class AlbumViewerPage extends HookConsumerWidget {
}
void onAddUsersPressed(Album album) async {
List<String>? sharedUserIds = await context.autoPush<List<String>?>(
List<String>? sharedUserIds =
await AutoRouter.of(context).push<List<String>?>(
SelectAdditionalUserForSharingRoute(album: album),
);
@@ -170,19 +171,11 @@ class AlbumViewerPage extends HookConsumerWidget {
return const SizedBox();
}
final String dateRangeText;
if (startDate.day == endDate.day &&
startDate.month == endDate.month &&
startDate.year == endDate.year) {
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";
}
final String startDateText = (startDate.year == endDate.year
? DateFormat.MMMd()
: DateFormat.yMMMd())
.format(startDate);
final String endDateText = DateFormat.yMMMd().format(endDate);
return Padding(
padding: EdgeInsets.only(
@@ -190,7 +183,7 @@ class AlbumViewerPage extends HookConsumerWidget {
bottom: album.shared ? 0.0 : 8.0,
),
child: Text(
dateRangeText,
"$startDateText - $endDateText",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@@ -202,7 +195,7 @@ class AlbumViewerPage extends HookConsumerWidget {
Widget buildSharedUserIconsRow(Album album) {
return GestureDetector(
onTap: () async {
await context.autoPush(AlbumOptionsRoute(album: album));
await AutoRouter.of(context).push(AlbumOptionsRoute(album: album));
ref.invalidate(albumDetailProvider(album.id));
},
child: SizedBox(
@@ -239,19 +232,6 @@ 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(
appBar: album.when(
data: (data) => AlbumViewerAppbar(
@@ -262,7 +242,6 @@ class AlbumViewerPage extends HookConsumerWidget {
selectionDisabled: disableSelection,
onAddPhotos: onAddPhotosPressed,
onAddUsers: onAddUsersPressed,
onActivities: onActivitiesPressed,
),
error: (error, stackTrace) => AppBar(title: const Text("Error")),
loading: () => AppBar(),
@@ -287,8 +266,6 @@ class AlbumViewerPage extends HookConsumerWidget {
],
),
isOwner: userId == data.ownerId,
sharedAlbumId:
data.shared && data.activityEnabled ? data.remoteId : null,
),
),
),

View File

@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/asset_viewer/providers/render_list.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
@@ -79,7 +78,7 @@ class AssetSelectionPage extends HookConsumerWidget {
canDeselect ? "share_done" : "share_add",
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
).tr(),
),

View File

@@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/providers/album.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>>(
initialAssets != null ? Set.from(initialAssets!) : const {},
);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
showSelectUserPage() async {
final bool? ok = await context.autoPush<bool?>(
SelectUserForSharingRoute(assets: selectedAssets.value),
);
final bool? ok = await AutoRouter.of(context)
.push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value));
if (ok == true) {
selectedAssets.value = {};
}
@@ -58,7 +58,7 @@ class CreateAlbumPage extends HookConsumerWidget {
onSelectPhotosButtonPressed() async {
AssetSelectionPageResult? selectedAsset =
await context.autoPush<AssetSelectionPageResult?>(
await AutoRouter.of(context).push<AssetSelectionPageResult?>(
AssetSelectionRoute(
existingAssets: selectedAssets.value,
canDeselect: true,
@@ -94,10 +94,10 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 200, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
style: context.textTheme.displayMedium?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
).tr(),
),
);
@@ -117,7 +117,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding:
const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: BorderSide(
color: context.isDarkTheme
color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
),
@@ -128,16 +128,16 @@ class CreateAlbumPage extends HookConsumerWidget {
onPressed: onSelectPhotosButtonPressed,
icon: Icon(
Icons.add_rounded,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
label: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'create_shared_album_page_share_select_photos',
style: context.textTheme.labelLarge?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
).tr(),
),
),
@@ -206,7 +206,7 @@ class CreateAlbumPage extends HookConsumerWidget {
selectedAssets.value = {};
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id));
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
}
}
@@ -214,19 +214,19 @@ class CreateAlbumPage extends HookConsumerWidget {
appBar: AppBar(
elevation: 0,
centerTitle: false,
backgroundColor: context.scaffoldBackgroundColor,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton(
onPressed: () {
selectedAssets.value = {};
context.autoPop();
AutoRouter.of(context).pop();
},
icon: const Icon(Icons.close_rounded),
),
title: Text(
'share_create_album',
style: context.textTheme.displayMedium?.copyWith(
color: context.primaryColor,
),
style: Theme.of(context).textTheme.displayMedium?.copyWith(
color: Theme.of(context).primaryColor,
),
).tr(),
actions: [
if (isSharedAlbum)
@@ -239,8 +239,8 @@ class CreateAlbumPage extends HookConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
color: albumTitleController.text.isEmpty
? context.themeData.disabledColor
: context.primaryColor,
? Theme.of(context).disabledColor
: Theme.of(context).primaryColor,
),
),
),
@@ -254,7 +254,7 @@ class CreateAlbumPage extends HookConsumerWidget {
'create_shared_album_page_create'.tr(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
),
@@ -265,7 +265,7 @@ class CreateAlbumPage extends HookConsumerWidget {
child: CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: context.scaffoldBackgroundColor,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 5,
automaticallyImplyLeading: false,
pinned: true,

View File

@@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.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';
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/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -21,7 +21,7 @@ class LibraryPage extends HookConsumerWidget {
final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final albums = ref.watch(albumProvider);
var isDarkTheme = context.isDarkTheme;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var settings = ref.watch(appSettingsServiceProvider);
useEffect(
@@ -96,14 +96,15 @@ class LibraryPage extends HookConsumerWidget {
padding: const EdgeInsets.only(right: 12.0),
child: Icon(
Icons.check,
color:
selected ? context.primaryColor : Colors.transparent,
color: selected
? Theme.of(context).primaryColor
: Colors.transparent,
),
),
Text(
option,
style: TextStyle(
color: selected ? context.primaryColor : null,
color: selected ? Theme.of(context).primaryColor : null,
fontSize: 12.0,
),
),
@@ -121,13 +122,13 @@ class LibraryPage extends HookConsumerWidget {
Icon(
Icons.swap_vert_rounded,
size: 18,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
Text(
options[selectedAlbumSortOrder.value],
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
fontSize: 12.0,
),
),
@@ -139,7 +140,7 @@ class LibraryPage extends HookConsumerWidget {
Widget buildCreateAlbumButton() {
return GestureDetector(
onTap: () {
context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
@@ -151,18 +152,18 @@ class LibraryPage extends HookConsumerWidget {
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: isDarkTheme
color: isDarkMode
? const Color.fromARGB(255, 53, 53, 53)
: const Color.fromARGB(255, 203, 203, 203),
),
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Icon(
Icons.add_rounded,
size: 28,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
),
@@ -200,21 +201,21 @@ class LibraryPage extends HookConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13.0,
color: isDarkTheme ? Colors.white : Colors.grey[800],
color: isDarkMode ? Colors.white : Colors.grey[800],
),
),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
side: BorderSide(
color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!,
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
),
alignment: Alignment.centerLeft,
),
icon: Icon(
icon,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
);
@@ -227,7 +228,7 @@ class LibraryPage extends HookConsumerWidget {
Widget? shareTrashButton() {
return trashEnabled
? InkWell(
onTap: () => context.autoPush(const TrashRoute()),
onTap: () => AutoRouter.of(context).push(const TrashRoute()),
borderRadius: BorderRadius.circular(12),
child: const Icon(
Icons.delete_rounded,
@@ -256,12 +257,12 @@ class LibraryPage extends HookConsumerWidget {
children: [
buildLibraryNavButton(
"library_page_favorites".tr(), Icons.favorite_border, () {
context.autoNavigate(const FavoritesRoute());
AutoRouter.of(context).navigate(const FavoritesRoute());
}),
const SizedBox(width: 12.0),
buildLibraryNavButton(
"library_page_archive".tr(), Icons.archive_outlined, () {
context.autoNavigate(const ArchiveRoute());
AutoRouter.of(context).navigate(const ArchiveRoute());
}),
],
),
@@ -305,7 +306,7 @@ class LibraryPage extends HookConsumerWidget {
return AlbumThumbnailCard(
album: sorted[index - 1],
onTap: () => context.autoPush(
onTap: () => AutoRouter.of(context).push(
AlbumViewerRoute(
albumId: sorted[index - 1].id,
),
@@ -347,7 +348,7 @@ class LibraryPage extends HookConsumerWidget {
childCount: local.length,
(context, index) => AlbumThumbnailCard(
album: local[index],
onTap: () => context.autoPush(
onTap: () => AutoRouter.of(context).push(
AlbumViewerRoute(
albumId: local[index].id,
),

View File

@@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/shared/models/album.dart';
import 'package:immich_mobile/shared/models/user.dart';
@@ -22,13 +22,14 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
final sharedUsersList = useState<Set<User>>({});
addNewUsersHandler() {
context.autoPop(sharedUsersList.value.map((e) => e.id).toList());
AutoRouter.of(context)
.pop(sharedUsersList.value.map((e) => e.id).toList());
}
buildTileIcon(User user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: context.primaryColor,
backgroundColor: Theme.of(context).primaryColor,
child: const Icon(
Icons.check_rounded,
size: 25,
@@ -49,7 +50,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withOpacity(0.15),
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
label: Text(
user.email,
style: const TextStyle(
@@ -123,7 +124,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
context.autoPop(null);
AutoRouter.of(context).pop(null);
},
),
actions: [

View File

@@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/shared_album.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();
// ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
context.autoPop(true);
context
.autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
AutoRouter.of(context).pop(true);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
}
ScaffoldMessenger(
@@ -50,7 +50,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
buildTileIcon(User user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: context.primaryColor,
backgroundColor: Theme.of(context).primaryColor,
child: const Icon(
Icons.check_rounded,
size: 25,
@@ -71,7 +71,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withOpacity(0.15),
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
label: Text(
user.email,
style: const TextStyle(
@@ -139,20 +139,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
appBar: AppBar(
title: Text(
'share_invite',
style: TextStyle(color: context.primaryColor),
style: TextStyle(color: Theme.of(context).primaryColor),
).tr(),
elevation: 0,
centerTitle: false,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () async {
context.autoPop();
AutoRouter.of(context).pop();
},
),
actions: [
TextButton(
style: TextButton.styleFrom(
foregroundColor: context.primaryColor,
foregroundColor: Theme.of(context).primaryColor,
),
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
child: const Text(
@@ -160,7 +160,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
// color: context.primaryColor,
// color: Theme.of(context).primaryColor,
),
).tr(),
),

View File

@@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
@@ -21,6 +21,7 @@ class SharingPage extends HookConsumerWidget {
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
final userId = ref.watch(currentUserProvider)?.id;
final partner = ref.watch(partnerSharedWithProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect(
() {
@@ -46,9 +47,8 @@ class SharingPage extends HookConsumerWidget {
album: sharedAlbums[index],
showOwner: true,
onTap: () {
context.autoPush(
AlbumViewerRoute(albumId: sharedAlbums[index].id),
);
AutoRouter.of(context)
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
},
);
},
@@ -79,11 +79,12 @@ class SharingPage extends HookConsumerWidget {
album.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color:
context.isDarkTheme ? context.primaryColor : Colors.black,
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isDarkMode
? Theme.of(context).primaryColor
: Colors.black,
),
),
subtitle: isOwner
? Text(
@@ -102,9 +103,8 @@ class SharingPage extends HookConsumerWidget {
)
: null,
onTap: () {
context.autoPush(
AlbumViewerRoute(albumId: sharedAlbums[index].id),
);
AutoRouter.of(context)
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
},
);
},
@@ -127,7 +127,8 @@ class SharingPage extends HookConsumerWidget {
Expanded(
child: ElevatedButton.icon(
onPressed: () {
context.autoPush(CreateAlbumRoute(isSharedAlbum: true));
AutoRouter.of(context)
.push(CreateAlbumRoute(isSharedAlbum: true));
},
icon: const Icon(
Icons.photo_album_outlined,
@@ -146,7 +147,8 @@ class SharingPage extends HookConsumerWidget {
const SizedBox(width: 12.0),
Expanded(
child: ElevatedButton.icon(
onPressed: () => context.autoPush(const SharedLinkRoute()),
onPressed: () =>
AutoRouter.of(context).push(const SharedLinkRoute()),
icon: const Icon(
Icons.link,
size: 20,
@@ -189,21 +191,21 @@ class SharingPage extends HookConsumerWidget {
child: Icon(
Icons.insert_photo_rounded,
size: 50,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'sharing_page_empty_list',
style: context.textTheme.displaySmall,
style: Theme.of(context).textTheme.displaySmall,
).tr(),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'sharing_page_description',
style: context.textTheme.bodyMedium,
style: Theme.of(context).textTheme.bodyMedium,
).tr(),
),
],
@@ -216,7 +218,7 @@ class SharingPage extends HookConsumerWidget {
Widget sharePartnerButton() {
return InkWell(
onTap: () => context.autoPush(const PartnerRoute()),
onTap: () => AutoRouter.of(context).push(const PartnerRoute()),
borderRadius: BorderRadius.circular(12),
child: const Icon(
Icons.swap_horizontal_circle_rounded,

View File

@@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -30,7 +30,7 @@ class ArchivePage extends HookConsumerWidget {
AppBar buildAppBar(String count) {
return AppBar(
leading: IconButton(
onPressed: () => context.autoPop(),
onPressed: () => AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
centerTitle: true,

View File

@@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.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/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
@@ -68,7 +67,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
gravity: ToastGravity.BOTTOM,
);
}
context.pop();
Navigator.of(buildContext).pop();
},
);
return const ShareDialog();

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart';
class AdvancedBottomSheet extends HookConsumerWidget {
@@ -12,6 +11,8 @@ class AdvancedBottomSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return SingleChildScrollView(
child: Card(
shape: const RoundedRectangleBorder(
@@ -39,9 +40,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
const SizedBox(height: 32.0),
Container(
decoration: BoxDecoration(
color: context.isDarkTheme
? Colors.grey[900]
: Colors.grey[200],
color: isDarkMode ? Colors.grey[900] : Colors.grey[200],
borderRadius: BorderRadius.circular(15.0),
),
child: Padding(
@@ -71,7 +70,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
icon: Icon(
Icons.copy,
size: 16.0,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
),

View File

@@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
@@ -20,7 +19,8 @@ class DescriptionInput extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final textColor = isDarkTheme ? Colors.white : Colors.black;
final controller = useTextEditingController();
final focusNode = useFocusNode();
final isFocus = useState(false);

View File

@@ -4,8 +4,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.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/map/ui/map_thumbnail.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -28,42 +26,12 @@ class ExifBottomSheet extends HookConsumerWidget {
exifInfo.latitude != 0 &&
exifInfo.longitude != 0;
String formatTimeZone(Duration d) =>
"GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
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 fileCreatedAt = asset.fileCreatedAt.toLocal();
final date = DateFormat.yMMMEd().format(fileCreatedAt);
final time = DateFormat.jm().format(fileCreatedAt);
final date = DateFormat.yMMMEd().format(dt);
final time = DateFormat.jm().format(dt);
timeZone ??= formatTimeZone(dt.timeZoneOffset);
return '$date$time $timeZone';
return '$date$time';
}
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
@@ -112,7 +80,8 @@ class ExifBottomSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset));
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
var textColor = context.isDarkTheme ? Colors.white : Colors.black;
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var textColor = isDarkTheme ? Colors.white : Colors.black;
buildMap() {
return Padding(
@@ -328,14 +297,9 @@ class ExifBottomSheet extends HookConsumerWidget {
fontWeight: FontWeight.bold,
),
),
subtitle: exifInfo.f != null ||
exifInfo.exposureSeconds != null ||
exifInfo.mm != null ||
exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
)
: null,
subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null ? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
) : null,
),
],
);
@@ -404,7 +368,7 @@ class ExifBottomSheet extends HookConsumerWidget {
data: (data) => DescriptionInput(asset: data),
error: (error, stackTrace) => Icon(
Icons.image_not_supported_outlined,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
loading: () => const SizedBox(
width: 75,

View File

@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.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/providers/asset.provider.dart';
@@ -17,8 +16,6 @@ class TopControlAppBar extends HookConsumerWidget {
required this.onFavorite,
required this.onUploadPressed,
required this.isOwner,
required this.shareAlbumId,
required this.onActivitiesPressed,
}) : super(key: key);
final Asset asset;
@@ -27,23 +24,14 @@ class TopControlAppBar extends HookConsumerWidget {
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onAddToAlbumPressed;
final VoidCallback onActivitiesPressed;
final Function(Asset) onFavorite;
final bool isPlayingMotionVideo;
final bool isOwner;
final String? shareAlbumId;
@override
Widget build(BuildContext context, WidgetRef ref) {
const double iconSize = 22.0;
final a = ref.watch(assetWatcher(asset)).value ?? asset;
final comments = shareAlbumId != null
? ref.watch(
activityStatisticsStateProvider(
(albumId: shareAlbumId!, assetId: asset.remoteId),
),
)
: 0;
Widget buildFavoriteButton(a) {
return IconButton(
@@ -106,34 +94,6 @@ 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() {
return IconButton(
onPressed: onUploadPressed,
@@ -147,7 +107,7 @@ class TopControlAppBar extends HookConsumerWidget {
Widget buildBackButton() {
return IconButton(
onPressed: () {
context.autoPop();
AutoRouter.of(context).pop();
},
icon: Icon(
Icons.arrow_back_ios_new_rounded,
@@ -170,7 +130,6 @@ class TopControlAppBar extends HookConsumerWidget {
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
if (shareAlbumId != null) buildActivitiesButton(),
buildMoreInfoButton(),
],
);

View File

@@ -8,7 +8,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.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/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
@@ -50,7 +49,6 @@ class GalleryViewerPage extends HookConsumerWidget {
final int heroOffset;
final bool showStack;
final bool isOwner;
final String? sharedAlbumId;
GalleryViewerPage({
super.key,
@@ -60,7 +58,6 @@ class GalleryViewerPage extends HookConsumerWidget {
this.heroOffset = 0,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
}) : controller = PageController(initialPage: initialIndex);
final PageController controller;
@@ -210,7 +207,7 @@ class GalleryViewerPage extends HookConsumerWidget {
if (isDeleted && isParent) {
if (totalAssets == 1) {
// Handle only one asset
context.autoPop();
AutoRouter.of(context).pop();
} else {
// Go to next page otherwise
controller.nextPage(
@@ -294,7 +291,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final ratio = d.dy / max(d.dx.abs(), 1);
if (d.dy > sensitivity && ratio > ratioThreshold) {
context.autoPop();
AutoRouter.of(context).pop();
} else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
showInfo();
}
@@ -309,7 +306,7 @@ class GalleryViewerPage extends HookConsumerWidget {
.watch(assetProvider.notifier)
.toggleArchive([asset], !asset.isArchived);
if (isParent) {
context.autoPop();
AutoRouter.of(context).pop();
return;
}
removeAssetFromStack();
@@ -330,19 +327,6 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
handleActivities() {
if (sharedAlbumId != null) {
context.autoPush(
ActivitiesRoute(
albumId: sharedAlbumId!,
assetId: asset().remoteId,
withAssetThumbs: false,
isOwner: isOwner,
),
);
}
}
buildAppBar() {
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
@@ -371,8 +355,6 @@ class GalleryViewerPage extends HookConsumerWidget {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}),
onAddToAlbumPressed: () => addToAlbum(asset()),
shareAlbumId: sharedAlbumId,
onActivitiesPressed: handleActivities,
),
),
),
@@ -461,8 +443,7 @@ class GalleryViewerPage extends HookConsumerWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
border: (stackIndex.value == -1 && index == 0) ||
index == stackIndex.value
border: index == stackIndex.value
? Border.all(
color: Colors.white,
width: 2,
@@ -515,7 +496,7 @@ class GalleryViewerPage extends HookConsumerWidget {
stackElements.elementAt(stackIndex.value),
);
Navigator.pop(ctx);
context.autoPop();
AutoRouter.of(context).pop();
},
title: const Text(
"viewer_stack_use_as_main_asset",
@@ -542,7 +523,7 @@ class GalleryViewerPage extends HookConsumerWidget {
childrenToRemove: [currentAsset],
);
Navigator.pop(ctx);
context.autoPop();
AutoRouter.of(context).pop();
} else {
await ref.read(assetStackServiceProvider).updateStack(
currentAsset,
@@ -570,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget {
childrenToRemove: stack,
);
Navigator.pop(ctx);
context.autoPop();
AutoRouter.of(context).pop();
},
title: const Text(
"viewer_unstack",
@@ -810,11 +791,6 @@ class GalleryViewerPage extends HookConsumerWidget {
localPosition = details.localPosition,
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes(
tag: isFromDto
? '${a.remoteId}-$heroOffset'
: a.id + heroOffset,
),
filterQuality: FilterQuality.high,
maxScale: 1.0,
minScale: 1.0,
@@ -830,8 +806,8 @@ class GalleryViewerPage extends HookConsumerWidget {
placeholder: Image(
image: provider,
fit: BoxFit.fitWidth,
height: context.height,
width: context.width,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
),
onVideoEnded: () {

View File

@@ -3,13 +3,14 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:video_player/video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@@ -45,7 +46,7 @@ class VideoViewerPage extends HookConsumerWidget {
),
error: (error, stackTrace) => Icon(
Icons.image_not_supported_outlined,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
loading: () => const Center(
child: SizedBox(
@@ -75,8 +76,8 @@ class VideoViewerPage extends HookConsumerWidget {
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
SizedBox(
height: context.height,
width: context.width,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: const Center(
child: ImmichLoadingIndicator(),
),
@@ -126,99 +127,128 @@ class VideoPlayer extends StatefulWidget {
}
class _VideoPlayerState extends State<VideoPlayer> {
late VideoPlayerController videoPlayerController;
ChewieController? chewieController;
// late VideoPlayerController videoPlayerController;
// ChewieController? chewieController;
// Create a [Player] to control playback.
late final player = Player();
// Create a [VideoController] to handle video output from [Player].
late final controller = VideoController(player);
@override
void initState() {
super.initState();
initializePlayer();
// initializePlayer();
videoPlayerController.addListener(() {
if (videoPlayerController.value.isInitialized) {
if (videoPlayerController.value.isPlaying) {
WakelockPlus.enable();
widget.onPlaying?.call();
} else if (!videoPlayerController.value.isPlaying) {
WakelockPlus.disable();
widget.onPaused?.call();
}
// videoPlayerController.addListener(() {
// if (videoPlayerController.value.isInitialized) {
// if (videoPlayerController.value.isPlaying) {
// WakelockPlus.enable();
// widget.onPlaying?.call();
// } else if (!videoPlayerController.value.isPlaying) {
// WakelockPlus.disable();
// widget.onPaused?.call();
// }
if (videoPlayerController.value.position ==
videoPlayerController.value.duration) {
WakelockPlus.disable();
widget.onVideoEnded();
}
}
});
}
// if (videoPlayerController.value.position ==
// videoPlayerController.value.duration) {
// WakelockPlus.disable();
// widget.onVideoEnded();
// }
// }
// });
Future<void> initializePlayer() async {
try {
videoPlayerController = widget.file == null
? VideoPlayerController.networkUrl(
Uri.parse(widget.url!),
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
)
: VideoPlayerController.file(widget.file!);
await videoPlayerController.initialize();
_createChewieController();
setState(() {});
} catch (e) {
debugPrint("ERROR initialize video player $e");
if (widget.file == null) {
player.open(
Media(
Uri.parse(widget.url!).toString(),
httpHeaders: {
"Authorization": "Bearer ${widget.jwtToken}",
},
),
);
} else {
player.open(
Media(
widget.file!.path,
),
);
}
}
_createChewieController() {
chewieController = ChewieController(
controlsSafeAreaMinimum: const EdgeInsets.only(
bottom: 100,
),
showOptions: true,
showControlsOnInitialize: false,
videoPlayerController: videoPlayerController,
autoPlay: true,
autoInitialize: true,
allowFullScreen: false,
allowedScreenSleep: false,
showControls: !widget.isMotionVideo,
customControls: const VideoPlayerControls(),
hideControlsTimer: const Duration(seconds: 5),
);
}
// Future<void> initializePlayer() async {
// try {
// videoPlayerController = widget.file == null
// ? VideoPlayerController.networkUrl(
// Uri.parse(widget.url!),
// httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
// )
// : VideoPlayerController.file(widget.file!);
// await videoPlayerController.initialize();
// _createChewieController();
// setState(() {});
// } catch (e) {
// debugPrint("ERROR initialize video player $e");
// }
// }
// _createChewieController() {
// chewieController = ChewieController(
// controlsSafeAreaMinimum: const EdgeInsets.only(
// bottom: 100,
// ),
// showOptions: true,
// showControlsOnInitialize: false,
// videoPlayerController: videoPlayerController,
// autoPlay: true,
// autoInitialize: true,
// allowFullScreen: false,
// allowedScreenSleep: false,
// showControls: !widget.isMotionVideo,
// customControls: const VideoPlayerControls(),
// hideControlsTimer: const Duration(seconds: 5),
// );
// }
@override
void dispose() {
super.dispose();
videoPlayerController.pause();
videoPlayerController.dispose();
chewieController?.dispose();
player.dispose();
// videoPlayerController.pause();
// videoPlayerController.dispose();
// chewieController?.dispose();
}
@override
Widget build(BuildContext context) {
if (chewieController?.videoPlayerController.value.isInitialized == true) {
return SizedBox(
child: Chewie(
controller: chewieController!,
),
);
} else {
return SizedBox(
height: context.height,
width: context.width,
child: Center(
child: Stack(
children: [
if (widget.placeholder != null) widget.placeholder!,
const Center(
child: ImmichLoadingIndicator(),
),
],
),
),
);
}
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Video(controller: controller),
);
// if (chewieController?.videoPlayerController.value.isInitialized == true) {
// return SizedBox(
// child: Chewie(
// controller: chewieController!,
// ),
// );
// } else {
// return SizedBox(
// height: MediaQuery.of(context).size.height,
// width: MediaQuery.of(context).size.width,
// child: Center(
// child: Stack(
// children: [
// if (widget.placeholder != null) widget.placeholder!,
// const Center(
// child: ImmichLoadingIndicator(),
// ),
// ],
// ),
// ),
// );
// }
}
}

View File

@@ -40,7 +40,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: 0,
cancelToken: CancellationToken(),
autoBackup: Store.get(StoreKey.autoBackup, false),
backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
backgroundBackup: false,
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
backupRequireCharging:
Store.get(StoreKey.backupRequireCharging, false),
@@ -171,7 +171,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state.backupRequireCharging,
);
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
} else {
state = state.copyWith(
backgroundBackup: wasEnabled,
@@ -384,9 +383,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
state = state.copyWith(backgroundBackup: isEnabled);
if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
Store.put(StoreKey.backgroundBackup, isEnabled);
}
if (state.backupProgress != BackUpProgressEnum.inBackground) {
await _getBackupAlbumsInfo();

View File

@@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.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/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -22,10 +22,10 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
final isDarkTheme = context.isDarkTheme;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
ColorFilter selectedFilter = ColorFilter.mode(
context.primaryColor.withAlpha(100),
Theme.of(context).primaryColor.withAlpha(100),
BlendMode.darken,
);
ColorFilter excludedFilter =
@@ -46,7 +46,7 @@ class AlbumInfoCard extends HookConsumerWidget {
fontWeight: FontWeight.bold,
),
).tr(),
backgroundColor: context.primaryColor,
backgroundColor: Theme.of(context).primaryColor,
);
} else if (isExcluded) {
return Chip(
@@ -194,7 +194,7 @@ class AlbumInfoCard extends HookConsumerWidget {
albumInfo.name,
style: TextStyle(
fontSize: 14,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
@@ -224,13 +224,13 @@ class AlbumInfoCard extends HookConsumerWidget {
),
IconButton(
onPressed: () {
context.autoPush(
AutoRouter.of(context).push(
AlbumPreviewRoute(album: albumInfo.albumEntity),
);
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
size: 24,
),
splashRadius: 25,

View File

@@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.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/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -25,13 +25,14 @@ class AlbumInfoListTile extends HookConsumerWidget {
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode(
context.primaryColor.withAlpha(100),
Theme.of(context).primaryColor.withAlpha(100),
BlendMode.darken,
);
ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var assetCount = useState(0);
@@ -55,11 +56,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
buildTileColor() {
if (isSelected) {
return context.isDarkTheme
? context.primaryColor.withAlpha(100)
: context.primaryColor.withAlpha(25);
return isDarkTheme
? Theme.of(context).primaryColor.withAlpha(100)
: Theme.of(context).primaryColor.withAlpha(25);
} else if (isExcluded) {
return context.isDarkTheme
return isDarkTheme
? Colors.red[300]?.withAlpha(150)
: Colors.red[100]?.withAlpha(150);
} else {
@@ -158,13 +159,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
subtitle: Text(assetCount.value.toString()),
trailing: IconButton(
onPressed: () {
context.autoPush(
AutoRouter.of(context).push(
AlbumPreviewRoute(album: albumInfo.albumEntity),
);
},
icon: Icon(
Icons.image_outlined,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
size: 24,
),
splashRadius: 25,

View File

@@ -1,6 +1,5 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class BackupInfoCard extends StatelessWidget {
final String title;
@@ -15,11 +14,13 @@ class BackupInfoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide(
color: context.isDarkTheme
color: isDarkMode
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,

View File

@@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/providers/backup.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,
onPressed: () {
context.autoPush(const FailedBackupStatusRoute());
AutoRouter.of(context).push(const FailedBackupStatusRoute());
},
);
}
@@ -61,7 +61,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
Widget buildAssetInfoTable() {
return Table(
border: TableBorder.all(
color: context.themeData.primaryColorLight,
color: Theme.of(context).primaryColorLight,
width: 1,
),
children: [
@@ -176,7 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
onTap: () => isShowThumbnail.value = true,
child: Icon(
Icons.image_outlined,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
size: 30,
),
),
@@ -206,7 +206,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
minHeight: 10.0,
value: uploadProgress / 100.0,
backgroundColor: Colors.grey,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
Text(

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.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:intl/intl.dart';
@@ -44,7 +43,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
subtitle: Text(
@@ -55,7 +54,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
),
leading: Icon(
Icons.bug_report,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
);
}

View File

@@ -1,9 +1,9 @@
import 'dart:typed_data';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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:photo_manager/photo_manager.dart';
@@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
],
),
leading: IconButton(
onPressed: () => context.autoPop(),
onPressed: () => AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
),

View File

@@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/ui/album_info_card.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 selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = context.isDarkTheme;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final allAlbums = ref.watch(backupProvider).availableAlbums;
// Albums which are displayed to the user
@@ -118,7 +118,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
fontWeight: FontWeight.bold,
),
),
backgroundColor: context.primaryColor,
backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
deleteIcon: const Icon(
Icons.cancel_rounded,
@@ -211,7 +211,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => context.autoPop(),
onPressed: () => AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
title: const Text(
@@ -315,7 +315,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
"backup_album_selection_page_albums_tap",
style: TextStyle(
fontSize: 12,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
@@ -325,7 +325,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
icon: Icon(
Icons.info,
size: 20,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
onPressed: () {
// show the dialog
@@ -342,7 +342,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
).tr(),
content: SingleChildScrollView(

View File

@@ -1,11 +1,11 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
@@ -49,6 +49,7 @@ class BackupControllerPage extends HookConsumerWidget {
!hasExclusiveAccess
? false
: true;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
final checkInProgress = useState(false);
useEffect(
@@ -150,7 +151,7 @@ class BackupControllerPage extends HookConsumerWidget {
return ListTile(
leading: Icon(
Icons.warning_rounded,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
title: const Text(
"Check for corrupt asset backups",
@@ -186,7 +187,7 @@ class BackupControllerPage extends HookConsumerWidget {
leading: isAutoBackup
? Icon(
Icons.cloud_done_rounded,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
)
: const Icon(Icons.cloud_off_rounded),
title: Text(
@@ -265,7 +266,7 @@ class BackupControllerPage extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
).tr(),
onPressed: () {
context.pop();
Navigator.of(context).pop();
},
),
],
@@ -278,7 +279,7 @@ class BackupControllerPage extends HookConsumerWidget {
final bool isBackgroundEnabled = backupState.backgroundBackup;
final bool isWifiRequired = backupState.backupRequireWifi;
final bool isChargingRequired = backupState.backupRequireCharging;
final Color activeColor = context.primaryColor;
final Color activeColor = Theme.of(context).primaryColor;
String formatBackupDelaySliderValue(double v) {
if (v == 0.0) {
@@ -409,7 +410,7 @@ class BackupControllerPage extends HookConsumerWidget {
max: 3.0,
divisions: 3,
label: formatBackupDelaySliderValue(triggerDelay.value),
activeColor: context.primaryColor,
activeColor: Theme.of(context).primaryColor,
),
),
ElevatedButton(
@@ -510,7 +511,7 @@ class BackupControllerPage extends HookConsumerWidget {
child: Text(
text.trim().substring(0, text.length - 2),
style: TextStyle(
color: context.primaryColor,
color: Theme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
@@ -522,7 +523,7 @@ class BackupControllerPage extends HookConsumerWidget {
child: Text(
"backup_controller_page_none_selected".tr(),
style: TextStyle(
color: context.primaryColor,
color: Theme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
@@ -561,7 +562,7 @@ class BackupControllerPage extends HookConsumerWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: context.isDarkTheme
color: isDarkMode
? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12,
width: 1,
@@ -591,7 +592,7 @@ class BackupControllerPage extends HookConsumerWidget {
),
trailing: ElevatedButton(
onPressed: () {
context.autoPush(const BackupAlbumSelectionRoute());
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
},
child: const Text(
"backup_controller_page_select",
@@ -677,7 +678,7 @@ class BackupControllerPage extends HookConsumerWidget {
leading: IconButton(
onPressed: () {
ref.watch(websocketProvider.notifier).listenUploadEvent();
context.autoPop(true);
AutoRouter.of(context).pop(true);
},
splashRadius: 24,
icon: const Icon(

View File

@@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.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:intl/intl.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -20,7 +20,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
),
leading: IconButton(
onPressed: () {
context.autoPop(true);
AutoRouter.of(context).pop(true);
},
splashRadius: 24,
icon: const Icon(
@@ -114,7 +114,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
),
),
),

View File

@@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart';
@@ -28,7 +28,7 @@ class FavoritesPage extends HookConsumerWidget {
AppBar buildAppBar() {
return AppBar(
leading: IconButton(
onPressed: () => context.autoPop(),
onPressed: () => AutoRouter.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_rounded),
),
centerTitle: true,

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class GroupDividerTitle extends ConsumerWidget {
const GroupDividerTitle({
@@ -52,7 +51,7 @@ class GroupDividerTitle extends ConsumerWidget {
child: multiselectEnabled && selected
? Icon(
Icons.check_circle_rounded,
color: context.primaryColor,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,

View File

@@ -34,7 +34,6 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool showDragScroll;
final bool showStack;
final bool isOwner;
final String? sharedAlbumId;
const ImmichAssetGrid({
super.key,
@@ -56,7 +55,6 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.showDragScroll = true,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
});
@override
@@ -122,7 +120,6 @@ class ImmichAssetGrid extends HookConsumerWidget {
showDragScroll: showDragScroll,
showStack: showStack,
isOwner: isOwner,
sharedAlbumId: sharedAlbumId,
),
);
}

View File

@@ -4,11 +4,10 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/utils/builtin_extensions.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart';
import 'group_divider_title.dart';
@@ -40,7 +39,6 @@ class ImmichAssetGridView extends StatefulWidget {
final bool showDragScroll;
final bool showStack;
final bool isOwner;
final String? sharedAlbumId;
const ImmichAssetGridView({
super.key,
@@ -62,7 +60,6 @@ class ImmichAssetGridView extends StatefulWidget {
this.showDragScroll = true,
this.showStack = false,
this.isOwner = true,
this.sharedAlbumId,
});
@override
@@ -144,7 +141,6 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
heroOffset: widget.heroOffset,
showStack: widget.showStack,
isOwner: widget.isOwner,
sharedAlbumId: widget.sharedAlbumId,
);
}
@@ -225,7 +221,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: context.textTheme.displayLarge?.color,
color: Theme.of(context).textTheme.displayLarge?.color,
),
),
);
@@ -373,7 +369,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
backgroundColor: context.themeData.hintColor,
backgroundColor: Theme.of(context).hintColor,
labelTextBuilder: _labelBuilder,
labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(milliseconds: 300),

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