mirror of
https://github.com/immich-app/immich.git
synced 2025-12-07 21:30:59 -08:00
Compare commits
20 Commits
feat/widge
...
fix/folder
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4adf5b24f8 | ||
|
|
eca9b56847 | ||
|
|
5b0575b956 | ||
|
|
05064f87f0 | ||
|
|
522cdbac99 | ||
|
|
9240bbc6ff | ||
|
|
3751f8bc57 | ||
|
|
88b8afb8d6 | ||
|
|
2e13543d5d | ||
|
|
bcfc967d77 | ||
|
|
7d0e8f50f7 | ||
|
|
c759233d8c | ||
|
|
bfe32c2bb9 | ||
|
|
6c7b2e4b5c | ||
|
|
7edbeb2ed6 | ||
|
|
4e59a55c1d | ||
|
|
c2d7337d12 | ||
|
|
c1b82bed9b | ||
|
|
9ca31abae9 | ||
|
|
ebcf133bea |
@@ -73,10 +73,10 @@ install_dependencies() {
|
||||
log "Installing dependencies"
|
||||
(
|
||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||
run_cmd make install-server
|
||||
run_cmd make install-sdk
|
||||
run_cmd make ci-server
|
||||
run_cmd make ci-sdk
|
||||
run_cmd make build-sdk
|
||||
run_cmd make install-web
|
||||
run_cmd make ci-web
|
||||
)
|
||||
log ""
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
build:
|
||||
target: dev-container-server
|
||||
env_file: !reset []
|
||||
hostname: immich-dev
|
||||
environment:
|
||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||
volumes: !override
|
||||
|
||||
@@ -6,7 +6,11 @@ design/
|
||||
docker/
|
||||
!docker/scripts
|
||||
docs/
|
||||
!docs/package.json
|
||||
!docs/package-lock.json
|
||||
e2e/
|
||||
!e2e/package.json
|
||||
!e2e/package-lock.json
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
|
||||
2
.github/workflows/build-mobile.yml
vendored
2
.github/workflows/build-mobile.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
|
||||
7
.github/workflows/cli.yml
vendored
7
.github/workflows/cli.yml
vendored
@@ -38,6 +38,9 @@ jobs:
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Prepare SDK
|
||||
run: npm ci --prefix ../open-api/typescript-sdk/
|
||||
- name: Build SDK
|
||||
@@ -67,7 +70,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
@@ -96,7 +99,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -76,6 +76,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -177,7 +177,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -188,6 +188,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -57,6 +57,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -32,6 +32,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Fix formatting
|
||||
run: make install-all && make format-all
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
name: release-apk-signed
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.IMMICH_VERSION }}
|
||||
|
||||
2
.github/workflows/sdk.yml
vendored
2
.github/workflows/sdk.yml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
with:
|
||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
- name: Build
|
||||
|
||||
6
.github/workflows/static_analysis.yml
vendored
6
.github/workflows/static_analysis.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Run DCM
|
||||
run: dcm analyze lib
|
||||
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||
working-directory: ./mobile
|
||||
|
||||
zizmor:
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
32
.github/workflows/test.yml
vendored
32
.github/workflows/test.yml
vendored
@@ -84,6 +84,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
@@ -125,6 +127,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -170,6 +174,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -208,6 +214,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -249,6 +257,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -282,6 +292,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix=web ci
|
||||
@@ -326,6 +338,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -369,6 +383,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
@@ -402,6 +418,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -450,6 +468,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
@@ -479,7 +499,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -496,7 +516,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
||||
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -568,6 +588,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './.github/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Run npm install
|
||||
run: npm ci
|
||||
@@ -609,6 +631,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install server dependencies
|
||||
run: npm --prefix=server ci
|
||||
@@ -644,7 +668,7 @@ jobs:
|
||||
contents: read
|
||||
services:
|
||||
postgres:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
@@ -670,6 +694,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
- name: Install server dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/weblate-lock.yml
vendored
2
.github/workflows/weblate-lock.yml
vendored
@@ -52,6 +52,6 @@ jobs:
|
||||
permissions: {}
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
5
Makefile
5
Makefile
@@ -48,6 +48,8 @@ audit-%:
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
|
||||
install-%:
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
|
||||
ci-%:
|
||||
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci
|
||||
build-cli: build-sdk
|
||||
build-web: build-sdk
|
||||
build-%: install-%
|
||||
@@ -81,7 +83,8 @@ test-medium-dev:
|
||||
docker exec -it immich_server /bin/sh -c "npm run test:medium"
|
||||
|
||||
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
|
||||
install-all: $(foreach M,$(MODULES),install-$M) ;
|
||||
install-all: $(foreach M,$(filter-out .github,$(MODULES)),install-$M) ;
|
||||
ci-all: $(foreach M,$(filter-out .github,$(MODULES)),ci-$M) ;
|
||||
check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
|
||||
lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
|
||||
format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
|
||||
|
||||
4
cli/package-lock.json
generated
4
cli/package-lock.json
generated
@@ -27,7 +27,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -122,7 +122,7 @@ services:
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -63,7 +63,7 @@ services:
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -94,7 +94,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055
|
||||
image: grafana/grafana:12.0.2-ubuntu@sha256:0512d81cdeaaff0e370a9aa66027b465d1f1f04379c3a9c801a905fabbdbc7a5
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:5f6a838e4e44c8e0e019d0ebfe3ee8952b69afc2809b2c25f7b0119641978e91
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
|
||||
|
||||
database:
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:9c704fb49ce27549df00f1b096cc93f8b0c959ef087507704d74954808f78a82
|
||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:3aef84a0a4fabbda17ef115c3019ba0c914ec73e9f6e59203674322d858b8eea
|
||||
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
6
e2e/package-lock.json
generated
6
e2e/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -66,7 +66,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -100,7 +100,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
||||
@@ -119,6 +119,16 @@ describe('/shared-links', () => {
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(`<meta property="og:image" content="https://my.immich.app`);
|
||||
});
|
||||
|
||||
it('should return 404 for an invalid shared link', async () => {
|
||||
const resp = await request(shareUrl).get(`/invalid-key`);
|
||||
expect(resp.status).toBe(404);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).not.toContain(`og:type`);
|
||||
expect(resp.text).not.toContain(`og:title`);
|
||||
expect(resp.text).not.toContain(`og:description`);
|
||||
expect(resp.text).not.toContain(`og:image`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /shared-links', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:d2621a9f74d31a8a60af19f97b09cc3ac54382c8680b6544018713a12ef6c048 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:ce3b954c9285a7a145cba620bae03db836ab890b6b9e0d05a3ca522ea00dfbc9 AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
@@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4faec156e35a5f345d57804d8858c6ba1cf6352ce5f4bffc11b7fdebdef46a38 /uv /uvx /bin/
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:cda0fdc9b6066975ba4c791597870d18bc3a441dfc18ab24c5e888c16e15780c /uv /uvx /bin/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
@@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||
fi
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cbe98fecf197fe5b085506 AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-openvino
|
||||
FROM python:3.11-slim-bookworm@sha256:9e1912aab0a30bbd9488eb79063f68f42a68ab0946cbe98fecf197fe5b085506 AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
141
machine-learning/uv.lock
generated
141
machine-learning/uv.lock
generated
@@ -517,16 +517,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.12"
|
||||
version = "0.115.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -900,7 +900,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.32.4"
|
||||
version = "0.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -912,9 +912,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494, upload-time = "2025-06-03T09:59:46.105Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101, upload-time = "2025-06-03T09:59:44.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1225,7 +1225,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.37.9"
|
||||
version = "2.37.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "configargparse" },
|
||||
@@ -1245,14 +1245,14 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/05/2bfdf19756c6a12f6f9513f75340ecf0595d83cab4d9fc91162225908e3d/locust-2.37.9.tar.gz", hash = "sha256:e43673b594ec5ecde4f9ba6e0d5c66c00d7c0ae93591951abe83e8d186c67175", size = 2252507, upload-time = "2025-06-05T09:26:58.186Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/19/66cdab585f7d4385be615d3792402fc75a1bed7519e5283adbe7133dbc78/locust-2.37.11.tar.gz", hash = "sha256:89c79bc599aa57160bd41dd3876e35d8b9dee5abded78e35008d01fd8f1640ed", size = 2252602, upload-time = "2025-06-23T08:22:23.922Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1c/0ece4176231c578e819d970ec08d124492833e50aafd171c582bcc414446/locust-2.37.9-py3-none-any.whl", hash = "sha256:e17da439f3a252d1fb6d4c34daf00d7e8b87e99d833a32e8a79f4f8ebb07767d", size = 2269084, upload-time = "2025-06-05T09:26:56.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2d/e5ae05911521bf84113be349d51b16d54589e986837d2d518f63434ea3ec/locust-2.37.11-py3-none-any.whl", hash = "sha256:b826f95fbfd5d9a32df6ab1b74672b88e65bbc33ec99fdc10af98079952ad517", size = 2269179, upload-time = "2025-06-23T08:22:21.067Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "locust-cloud"
|
||||
version = "1.23.1"
|
||||
version = "1.24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "configargparse" },
|
||||
@@ -1262,9 +1262,9 @@ dependencies = [
|
||||
{ name = "python-socketio", extra = ["client"] },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/7c/d9cbbd051490aeedfbd6ddda8ad48f77dd848ee490f6ebd166d20db5911e/locust_cloud-1.23.1.tar.gz", hash = "sha256:a09161752b8c9a9205e97cef5223ee3ad967bc2d91c52d61952aaa3da6802a55", size = 450937, upload-time = "2025-06-05T06:07:53.773Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/77/bda24167a2b763ba5d3cad1f3fa2a938f5273e51a61bffdbc8dc2e3ba24d/locust_cloud-1.24.2.tar.gz", hash = "sha256:a2656537ff367e6d4d4673477ba9e81ed73a8423a71573cd2512248740eded77", size = 451122, upload-time = "2025-06-23T11:08:00.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/01/5af43edee540e38ba0ee0a2e3beb72c50073e0f646bb543a8b34650315e3/locust_cloud-1.23.1-py3-none-any.whl", hash = "sha256:11677895c6ed6d0beef1b425a6f04f10ea2cfcaeaefbf00a97fb3c9134296e54", size = 408323, upload-time = "2025-06-05T06:07:51.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/38/8cda8aa1c6dfe5c5abbf69a9b10c03585c37eff64ca92733a291806052ac/locust_cloud-1.24.2-py3-none-any.whl", hash = "sha256:64a5e6f2bf0a1a012d9805291d44fb57e57535c2b5c0fa5bc87ba0d7cce9ef9c", size = 408594, upload-time = "2025-06-23T11:07:59.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1415,7 +1415,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.16.0"
|
||||
version = "1.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mypy-extensions" },
|
||||
@@ -1423,33 +1423,33 @@ dependencies = [
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1834,7 +1834,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.5"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
@@ -1842,9 +1842,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1977,7 +1977,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.0"
|
||||
version = "8.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -1988,9 +1988,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2007,15 +2007,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "6.1.1"
|
||||
version = "6.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage", extra = ["toml"] },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2303,27 +2304,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.13"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2558,14 +2559,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.32.0.20250602"
|
||||
version = "2.32.4.20250611"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/b0/5321e6eeba5d59e4347fcf9bf06a5052f085c3aa0f4876230566d6a4dc97/types_requests-2.32.0.20250602.tar.gz", hash = "sha256:ee603aeefec42051195ae62ca7667cd909a2f8128fdf8aad9e8a5219ecfab3bf", size = 23042, upload-time = "2025-06-02T03:15:02.958Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/18/9b782980e575c6581d5c0c1c99f4c6f89a1d7173dad072ee96b2756c02e6/types_requests-2.32.0.20250602-py3-none-any.whl", hash = "sha256:f4f335f87779b47ce10b8b8597b409130299f6971ead27fead4fe7ba6ea3e726", size = 20638, upload-time = "2025-06-02T03:15:01.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -146,6 +146,7 @@ dart_code_metrics:
|
||||
# - no-empty-block
|
||||
# - no-equal-then-else
|
||||
# - prefer-correct-test-file-name
|
||||
- prefer-const-border-radius
|
||||
# - prefer-match-file-name
|
||||
# - prefer-return-await
|
||||
# - avoid-self-assignment
|
||||
@@ -290,7 +291,8 @@ dart_code_metrics:
|
||||
# Style
|
||||
# - prefer-trailing-comma
|
||||
# - unnecessary-trailing-comma
|
||||
# - prefer-declaring-const-constructor
|
||||
- prefer-declaring-const-constructor
|
||||
# - prefer-single-widget-per-file
|
||||
- prefer-switch-expression
|
||||
# - prefer-prefixed-global-constants
|
||||
# - prefer-correct-callback-field-name
|
||||
|
||||
@@ -90,6 +90,35 @@
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- immich:// URL scheme handling -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="immich" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- my.immich.app deep link -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:path="/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/albums/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/memories/" />
|
||||
<data
|
||||
android:host="my.immich.app"
|
||||
android:pathPrefix="/photos/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'general_helper.dart';
|
||||
class ImmichTestLoginHelper {
|
||||
final WidgetTester tester;
|
||||
|
||||
ImmichTestLoginHelper(this.tester);
|
||||
const ImmichTestLoginHelper(this.tester);
|
||||
|
||||
Future<void> waitForLoginScreen() async {
|
||||
await pumpUntilFound(tester, find.text("Login"));
|
||||
@@ -60,11 +60,11 @@ class ImmichTestLoginHelper {
|
||||
await tester.tap(button);
|
||||
}
|
||||
|
||||
Future<void> assertLoginSuccess({int timeoutSeconds = 15}) async {
|
||||
Future<void> assertLoginSuccess() async {
|
||||
await pumpUntilFound(tester, find.text("home_page_building_timeline".tr()));
|
||||
}
|
||||
|
||||
Future<void> assertLoginFailed({int timeoutSeconds = 15}) async {
|
||||
Future<void> assertLoginFailed() async {
|
||||
await pumpUntilFound(tester, find.text("login_form_failed_login".tr()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +86,23 @@
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Share Extension</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Deep Link</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>immich</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>210</string>
|
||||
@@ -120,6 +132,8 @@
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
@@ -166,8 +180,6 @@
|
||||
<true />
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true />
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>We need local network permission to connect to the local server using IP address and
|
||||
allow the casting feature to work</string>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:my.immich.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:my.immich.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
|
||||
@@ -24,27 +24,11 @@ struct ImageEntry: TimelineEntry {
|
||||
struct ImmichWidgetView: View {
|
||||
var entry: ImageEntry
|
||||
|
||||
func getErrorText(_ error: WidgetError?) -> String {
|
||||
switch error {
|
||||
case .noLogin:
|
||||
return "Login to Immich"
|
||||
|
||||
case .fetchFailed:
|
||||
return "Unable to connect to your Immich instance"
|
||||
|
||||
case .albumNotFound:
|
||||
return "Album not found"
|
||||
|
||||
default:
|
||||
return "An unknown error occured"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if entry.image == nil {
|
||||
VStack {
|
||||
Image("LaunchImage")
|
||||
Text(getErrorText(entry.error))
|
||||
Text(entry.error?.errorDescription ?? "")
|
||||
.minimumScaleFactor(0.25)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -8,6 +8,32 @@ enum WidgetError: Error {
|
||||
case unknown
|
||||
case albumNotFound
|
||||
case unableToResize
|
||||
case invalidImage
|
||||
case invalidURL
|
||||
}
|
||||
|
||||
extension WidgetError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .noLogin:
|
||||
return "Login to Immich"
|
||||
|
||||
case .fetchFailed:
|
||||
return "Unable to connect to your Immich instance"
|
||||
|
||||
case .albumNotFound:
|
||||
return "Album not found"
|
||||
|
||||
case .invalidURL:
|
||||
return "An invalid URL was used"
|
||||
|
||||
case .invalidImage:
|
||||
return "An invalid image was received"
|
||||
|
||||
default:
|
||||
return "An unknown error occured"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AssetType: String, Codable {
|
||||
@@ -17,14 +43,6 @@ enum AssetType: String, Codable {
|
||||
case other = "OTHER"
|
||||
}
|
||||
|
||||
struct ServerWellKnown: Codable {
|
||||
struct APIInfo: Codable{
|
||||
let endpoint: String
|
||||
}
|
||||
|
||||
let api: APIInfo
|
||||
}
|
||||
|
||||
struct SearchResult: Codable {
|
||||
let id: String
|
||||
let type: AssetType
|
||||
@@ -65,7 +83,7 @@ class ImmichAPI {
|
||||
init() async throws {
|
||||
// fetch the credentials from the UserDefaults store that dart placed here
|
||||
guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
|
||||
var serverURL = defaults.string(forKey: "widget_server_url"),
|
||||
let serverURL = defaults.string(forKey: "widget_server_url"),
|
||||
let sessionKey = defaults.string(forKey: "widget_auth_token")
|
||||
else {
|
||||
throw WidgetError.noLogin
|
||||
@@ -74,55 +92,13 @@ class ImmichAPI {
|
||||
if serverURL == "" || sessionKey == "" {
|
||||
throw WidgetError.noLogin
|
||||
}
|
||||
|
||||
// migrate the server list value to a JSON array if it is not already
|
||||
if !serverURL.starts(with: "[") {
|
||||
let newServerList = "[\"\(serverURL)\"]"
|
||||
defaults.set(newServerList, forKey: "widget_server_url")
|
||||
serverURL = newServerList
|
||||
}
|
||||
|
||||
guard let urls = try? JSONDecoder().decode([String].self, from: serverURL.data(using: .utf8)!) else {
|
||||
throw WidgetError.noLogin
|
||||
}
|
||||
|
||||
for url in urls {
|
||||
guard let endpointURL = URL(string: url) else { continue }
|
||||
|
||||
if let apiURL = await Self.validateServer(at: endpointURL) {
|
||||
serverConfig = ServerConfig(
|
||||
serverEndpoint: apiURL.absoluteString,
|
||||
sessionKey: sessionKey
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
throw WidgetError.fetchFailed
|
||||
|
||||
serverConfig = ServerConfig(
|
||||
serverEndpoint: serverURL,
|
||||
sessionKey: sessionKey
|
||||
)
|
||||
}
|
||||
|
||||
private static func validateServer(at endpointURL: URL) async -> URL? {
|
||||
// build a URL that is only scheme, host, and port
|
||||
var components = URLComponents()
|
||||
components.scheme = endpointURL.scheme
|
||||
components.host = endpointURL.host
|
||||
components.port = endpointURL.port
|
||||
|
||||
guard let baseURL = components.url else { return nil }
|
||||
|
||||
var pingURL = baseURL
|
||||
pingURL.appendPathComponent(".well-known")
|
||||
pingURL.appendPathComponent("immich")
|
||||
|
||||
guard let (serverPingJSON, _) = try? await URLSession.shared.data(from: pingURL) else { return nil }
|
||||
guard let apiInfo = try? JSONDecoder().decode(ServerWellKnown.self, from: serverPingJSON) else { return nil }
|
||||
|
||||
var apiURL = baseURL
|
||||
apiURL.appendPathComponent(apiInfo.api.endpoint)
|
||||
|
||||
return apiURL
|
||||
}
|
||||
|
||||
private func buildRequestURL(
|
||||
serverConfig: ServerConfig,
|
||||
endpoint: String,
|
||||
@@ -196,7 +172,7 @@ class ImmichAPI {
|
||||
return try JSONDecoder().decode([MemoryResult].self, from: data)
|
||||
}
|
||||
|
||||
func fetchImage(asset: SearchResult) async throws -> UIImage {
|
||||
func fetchImage(asset: SearchResult) async throws(WidgetError) -> UIImage {
|
||||
let thumbnailParams = [URLQueryItem(name: "size", value: "preview")]
|
||||
let assetEndpoint = "/assets/" + asset.id + "/thumbnail"
|
||||
|
||||
@@ -207,16 +183,24 @@ class ImmichAPI {
|
||||
params: thumbnailParams
|
||||
)
|
||||
else {
|
||||
throw URLError(.badURL)
|
||||
throw .invalidURL
|
||||
}
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithURL(fetchURL as CFURL, nil) else {
|
||||
throw .invalidURL
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: fetchURL)
|
||||
|
||||
guard let img = UIImage(data: data) else {
|
||||
throw URLError(.badServerResponse)
|
||||
let decodeOptions: [NSString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 400,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true
|
||||
]
|
||||
|
||||
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions as CFDictionary) else {
|
||||
throw .fetchFailed
|
||||
}
|
||||
|
||||
return img
|
||||
return UIImage(cgImage: thumbnail)
|
||||
}
|
||||
|
||||
func fetchAlbums() async throws -> [Album] {
|
||||
|
||||
@@ -20,11 +20,8 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
completion: @escaping @Sendable (ImageEntry) -> Void
|
||||
) {
|
||||
Task {
|
||||
var api: ImmichAPI
|
||||
do {
|
||||
api = try await ImmichAPI()
|
||||
} catch let error as WidgetError {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: error))
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,13 +79,9 @@ struct ImmichMemoryProvider: TimelineProvider {
|
||||
Task {
|
||||
var entries: [ImageEntry] = []
|
||||
let now = Date()
|
||||
|
||||
|
||||
var api: ImmichAPI
|
||||
do {
|
||||
api = try await ImmichAPI()
|
||||
} catch let error as WidgetError {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: error))
|
||||
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
||||
completion(Timeline(entries: entries, policy: .atEnd))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,15 +63,10 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
) async
|
||||
-> ImageEntry
|
||||
{
|
||||
var api: ImmichAPI
|
||||
do {
|
||||
api = try await ImmichAPI()
|
||||
} catch let error as WidgetError {
|
||||
return ImageEntry(date: Date(), image: nil, error: error)
|
||||
} catch {
|
||||
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
return ImageEntry(date: Date(), image: nil, error: .noLogin)
|
||||
}
|
||||
|
||||
|
||||
guard
|
||||
let randomImage = try? await api.fetchSearchResults(
|
||||
with: SearchFilters(size: 1)
|
||||
@@ -105,21 +100,15 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
|
||||
let now = Date()
|
||||
|
||||
// If we don't have a server config, return an entry with an error
|
||||
var api: ImmichAPI
|
||||
do {
|
||||
api = try await ImmichAPI()
|
||||
} catch let error as WidgetError {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: error))
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
} catch {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
|
||||
guard let api = try? await ImmichAPI() else {
|
||||
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
|
||||
return Timeline(entries: entries, policy: .atEnd)
|
||||
}
|
||||
|
||||
// nil if album is NONE or nil
|
||||
let albumId =
|
||||
configuration.album?.id != "NONE" ? configuration.album?.id : nil
|
||||
let albumName: String? = albumId != nil ? configuration.album?.albumName : nil
|
||||
var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
|
||||
|
||||
if albumId != nil {
|
||||
// make sure the album exists on server, otherwise show error
|
||||
|
||||
@@ -4,6 +4,8 @@ sealed class ImmichErrors {
|
||||
}
|
||||
|
||||
class NoResponseDtoError extends ImmichErrors implements Exception {
|
||||
const NoResponseDtoError();
|
||||
|
||||
@override
|
||||
String toString() => "Response Dto is null";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Person {
|
||||
Person({
|
||||
const Person({
|
||||
required this.id,
|
||||
this.birthDate,
|
||||
required this.isHidden,
|
||||
|
||||
@@ -554,18 +554,12 @@ class Asset {
|
||||
}""";
|
||||
}
|
||||
|
||||
static getVisibility(AssetVisibility visibility) {
|
||||
switch (visibility) {
|
||||
case AssetVisibility.timeline:
|
||||
return AssetVisibilityEnum.timeline;
|
||||
case AssetVisibility.archive:
|
||||
return AssetVisibilityEnum.archive;
|
||||
case AssetVisibility.hidden:
|
||||
return AssetVisibilityEnum.hidden;
|
||||
case AssetVisibility.locked:
|
||||
return AssetVisibilityEnum.locked;
|
||||
}
|
||||
}
|
||||
static getVisibility(AssetVisibility visibility) => switch (visibility) {
|
||||
AssetVisibility.archive => AssetVisibilityEnum.archive,
|
||||
AssetVisibility.hidden => AssetVisibilityEnum.hidden,
|
||||
AssetVisibility.locked => AssetVisibilityEnum.locked,
|
||||
AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline,
|
||||
};
|
||||
}
|
||||
|
||||
enum AssetType {
|
||||
|
||||
@@ -11,7 +11,7 @@ class SSLClientCertStoreVal {
|
||||
final Uint8List data;
|
||||
final String? password;
|
||||
|
||||
SSLClientCertStoreVal(this.data, this.password);
|
||||
const SSLClientCertStoreVal(this.data, this.password);
|
||||
|
||||
void save() {
|
||||
final b64Str = base64Encode(data);
|
||||
|
||||
@@ -5,7 +5,7 @@ class ApiRepository {
|
||||
|
||||
Future<T> checkNull<T>(Future<T?> future) async {
|
||||
final response = await future;
|
||||
if (response == null) throw NoResponseDtoError();
|
||||
if (response == null) throw const NoResponseDtoError();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -17,6 +18,7 @@ import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provide
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -26,6 +28,7 @@ import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||
import 'package:immich_mobile/utils/download.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
@@ -169,6 +172,31 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
);
|
||||
}
|
||||
|
||||
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
|
||||
final deepLinkHandler = ref.read(deepLinkServiceProvider);
|
||||
final currentRouteName = ref.read(currentRouteNameProvider.notifier).state;
|
||||
|
||||
if (deepLink.uri.scheme == "immich") {
|
||||
final proposedRoute = await deepLinkHandler.handleScheme(
|
||||
deepLink,
|
||||
currentRouteName == SplashScreenRoute.name,
|
||||
);
|
||||
|
||||
return proposedRoute;
|
||||
}
|
||||
|
||||
if (deepLink.uri.host == "my.immich.app") {
|
||||
final proposedRoute = await deepLinkHandler.handleMyImmichApp(
|
||||
deepLink,
|
||||
currentRouteName == SplashScreenRoute.name,
|
||||
);
|
||||
|
||||
return proposedRoute;
|
||||
}
|
||||
|
||||
return DeepLink.path(deepLink.path);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -220,8 +248,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
colorScheme: immichTheme.light,
|
||||
locale: context.locale,
|
||||
),
|
||||
routeInformationParser: router.defaultRouteParser(),
|
||||
routerDelegate: router.delegate(
|
||||
routerConfig: router.config(
|
||||
deepLinkBuilder: _deepLinkBuilder,
|
||||
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ class AlbumViewerPageState {
|
||||
final String editTitleText;
|
||||
final String editDescriptionText;
|
||||
|
||||
AlbumViewerPageState({
|
||||
const AlbumViewerPageState({
|
||||
required this.isEditAlbum,
|
||||
required this.editTitleText,
|
||||
required this.editDescriptionText,
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
class AssetSelectionPageResult {
|
||||
final Set<Asset> selectedAssets;
|
||||
|
||||
AssetSelectionPageResult({
|
||||
const AssetSelectionPageResult({
|
||||
required this.selectedAssets,
|
||||
});
|
||||
@override
|
||||
|
||||
@@ -7,7 +7,7 @@ class AuthState {
|
||||
final bool isAdmin;
|
||||
final String profileImagePath;
|
||||
|
||||
AuthState({
|
||||
const AuthState({
|
||||
required this.deviceId,
|
||||
required this.userId,
|
||||
required this.userEmail,
|
||||
|
||||
@@ -5,7 +5,7 @@ class AuxilaryEndpoint {
|
||||
final String url;
|
||||
final AuxCheckStatus status;
|
||||
|
||||
AuxilaryEndpoint({
|
||||
const AuxilaryEndpoint({
|
||||
required this.url,
|
||||
required this.status,
|
||||
});
|
||||
@@ -55,7 +55,7 @@ class AuxilaryEndpoint {
|
||||
|
||||
class AuxCheckStatus {
|
||||
final String name;
|
||||
AuxCheckStatus({
|
||||
const AuxCheckStatus({
|
||||
required this.name,
|
||||
});
|
||||
const AuxCheckStatus._(this.name);
|
||||
|
||||
@@ -13,7 +13,7 @@ class LoginResponse {
|
||||
|
||||
final String userId;
|
||||
|
||||
LoginResponse({
|
||||
const LoginResponse({
|
||||
required this.accessToken,
|
||||
required this.isAdmin,
|
||||
required this.name,
|
||||
|
||||
@@ -4,7 +4,7 @@ class AvailableAlbum {
|
||||
final Album album;
|
||||
final int assetCount;
|
||||
final DateTime? lastBackup;
|
||||
AvailableAlbum({
|
||||
const AvailableAlbum({
|
||||
required this.album,
|
||||
required this.assetCount,
|
||||
this.lastBackup,
|
||||
|
||||
@@ -9,7 +9,7 @@ class CurrentUploadAsset {
|
||||
final int? fileSize;
|
||||
final bool? iCloudAsset;
|
||||
|
||||
CurrentUploadAsset({
|
||||
const CurrentUploadAsset({
|
||||
required this.id,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileName,
|
||||
|
||||
@@ -5,7 +5,7 @@ class SuccessUploadAsset {
|
||||
final String remoteAssetId;
|
||||
final bool isDuplicate;
|
||||
|
||||
SuccessUploadAsset({
|
||||
const SuccessUploadAsset({
|
||||
required this.candidate,
|
||||
required this.remoteAssetId,
|
||||
required this.isDuplicate,
|
||||
|
||||
@@ -10,7 +10,7 @@ class DownloadInfo {
|
||||
// enum
|
||||
final TaskStatus status;
|
||||
|
||||
DownloadInfo({
|
||||
const DownloadInfo({
|
||||
required this.fileName,
|
||||
required this.progress,
|
||||
required this.status,
|
||||
@@ -71,7 +71,7 @@ class DownloadState {
|
||||
final TaskStatus downloadStatus;
|
||||
final Map<String, DownloadInfo> taskProgress;
|
||||
final bool showProgress;
|
||||
DownloadState({
|
||||
const DownloadState({
|
||||
required this.downloadStatus,
|
||||
required this.taskProgress,
|
||||
required this.showProgress,
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
||||
class RecursiveFolder extends RootFolder {
|
||||
final String name;
|
||||
|
||||
RecursiveFolder({
|
||||
const RecursiveFolder({
|
||||
required this.name,
|
||||
required super.path,
|
||||
required super.subfolders,
|
||||
|
||||
@@ -4,7 +4,7 @@ class RootFolder {
|
||||
final List<RecursiveFolder> subfolders;
|
||||
final String path;
|
||||
|
||||
RootFolder({
|
||||
const RootFolder({
|
||||
required this.subfolders,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
@@ -8,4 +8,6 @@ class MapAssetsInBoundsUpdated extends MapEvent {
|
||||
const MapAssetsInBoundsUpdated(this.assetRemoteIds);
|
||||
}
|
||||
|
||||
class MapCloseBottomSheet extends MapEvent {}
|
||||
class MapCloseBottomSheet extends MapEvent {
|
||||
const MapCloseBottomSheet();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:openapi/api.dart';
|
||||
class MapMarker {
|
||||
final LatLng latLng;
|
||||
final String assetRemoteId;
|
||||
MapMarker({
|
||||
const MapMarker({
|
||||
required this.latLng,
|
||||
required this.assetRemoteId,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ class MapState {
|
||||
final AsyncValue<String> lightStyleFetched;
|
||||
final AsyncValue<String> darkStyleFetched;
|
||||
|
||||
MapState({
|
||||
const MapState({
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.showFavoriteOnly = false,
|
||||
this.includeArchived = false,
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
class Memory {
|
||||
final String title;
|
||||
final List<Asset> assets;
|
||||
Memory({
|
||||
const Memory({
|
||||
required this.title,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ class SearchCuratedContent {
|
||||
/// The id to lookup the asset from the server
|
||||
final String id;
|
||||
|
||||
SearchCuratedContent({
|
||||
const SearchCuratedContent({
|
||||
required this.label,
|
||||
required this.id,
|
||||
this.subtitle,
|
||||
|
||||
@@ -6,7 +6,7 @@ class SearchResult {
|
||||
final List<Asset> assets;
|
||||
final int? nextPage;
|
||||
|
||||
SearchResult({
|
||||
const SearchResult({
|
||||
required this.assets,
|
||||
this.nextPage,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ class SearchResultPageState {
|
||||
final bool isSmart;
|
||||
final List<Asset> searchResult;
|
||||
|
||||
SearchResultPageState({
|
||||
const SearchResultPageState({
|
||||
required this.isLoading,
|
||||
required this.isSuccess,
|
||||
required this.isError,
|
||||
|
||||
@@ -13,7 +13,7 @@ class ServerInfo {
|
||||
final bool isNewReleaseAvailable;
|
||||
final String versionMismatchErrorMessage;
|
||||
|
||||
ServerInfo({
|
||||
const ServerInfo({
|
||||
required this.serverVersion,
|
||||
required this.latestVersion,
|
||||
required this.serverFeatures,
|
||||
|
||||
@@ -105,7 +105,9 @@ class AlbumsPage extends HookConsumerWidget {
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||
@@ -301,7 +303,9 @@ class QuickFilterButton extends StatelessWidget {
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.onSurface.withAlpha(25),
|
||||
width: 1,
|
||||
@@ -334,8 +338,10 @@ class SortButton extends ConsumerWidget {
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(
|
||||
@@ -384,8 +390,10 @@ class SortButton extends ConsumerWidget {
|
||||
: Colors.transparent,
|
||||
),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -246,8 +246,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
),
|
||||
elevation: 5,
|
||||
title: Text(
|
||||
|
||||
@@ -147,7 +147,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
|
||||
@@ -42,9 +42,11 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
||||
vertical: 4,
|
||||
),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15), // if you need this
|
||||
side: const BorderSide(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(15), // if you need this
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Colors.black12,
|
||||
width: 1,
|
||||
),
|
||||
|
||||
@@ -60,7 +60,9 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(15.0),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -99,7 +101,9 @@ class AppLogDetailPage extends HookConsumerWidget {
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(15.0),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
||||
@@ -120,8 +120,10 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
),
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
|
||||
@@ -90,8 +90,10 @@ class DownloadTaskTile extends StatelessWidget {
|
||||
width: context.width - 32,
|
||||
child: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
minVerticalPadding: 18,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||
@@ -11,7 +12,6 @@ import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
enum SettingSection {
|
||||
advanced(
|
||||
@@ -85,12 +85,13 @@ class SettingsPage extends StatelessWidget {
|
||||
centerTitle: false,
|
||||
title: const Text('settings').tr(),
|
||||
),
|
||||
body: context.isMobile ? _MobileLayout() : _TabletLayout(),
|
||||
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileLayout extends StatelessWidget {
|
||||
const _MobileLayout();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
@@ -147,6 +148,7 @@ class _MobileLayout extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _TabletLayout extends HookWidget {
|
||||
const _TabletLayout();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedSection =
|
||||
|
||||
@@ -72,7 +72,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.router.current.name != ShareIntentRoute.name) {
|
||||
if (context.router.current.name == SplashScreenRoute.name) {
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
}
|
||||
|
||||
|
||||
183
mobile/lib/pages/common/tab_shell.page.dart
Normal file
183
mobile/lib/pages/common/tab_shell.page.dart
Normal file
@@ -0,0 +1,183 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
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/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class TabShellPage extends ConsumerWidget {
|
||||
const TabShellPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
|
||||
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||
if (!isProcessing) return icon;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -18,
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void onNavigationSelected(TabsRouter router, int index) {
|
||||
// On Photos page menu tapped
|
||||
if (router.activeIndex == 0 && index == 0) {
|
||||
scrollToTopNotifierProvider.scrollToTop();
|
||||
}
|
||||
|
||||
// On Search page tapped
|
||||
if (router.activeIndex == 1 && index == 1) {
|
||||
ref.read(searchInputFocusProvider).requestFocus();
|
||||
}
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
router.setActiveIndex(index);
|
||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||
}
|
||||
|
||||
final navigationDestinations = [
|
||||
NavigationDestination(
|
||||
label: 'photos'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_library_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: false,
|
||||
icon: Icon(
|
||||
Icons.photo_library,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'search'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.search_rounded,
|
||||
),
|
||||
selectedIcon: Icon(
|
||||
Icons.search,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'albums'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.photo_album_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: false,
|
||||
icon: Icon(
|
||||
Icons.photo_album_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationDestination(
|
||||
label: 'library'.tr(),
|
||||
icon: const Icon(
|
||||
Icons.space_dashboard_outlined,
|
||||
),
|
||||
selectedIcon: buildIcon(
|
||||
isProcessing: false,
|
||||
icon: Icon(
|
||||
Icons.space_dashboard_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
Widget bottomNavigationBar(TabsRouter tabsRouter) {
|
||||
return NavigationBar(
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: (index) =>
|
||||
onNavigationSelected(tabsRouter, index),
|
||||
destinations: navigationDestinations,
|
||||
);
|
||||
}
|
||||
|
||||
Widget navigationRail(TabsRouter tabsRouter) {
|
||||
return NavigationRail(
|
||||
destinations: navigationDestinations
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: e.icon,
|
||||
label: Text(e.label),
|
||||
selectedIcon: e.selectedIcon,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) =>
|
||||
onNavigationSelected(tabsRouter, index),
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
groupAlignment: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||
return AutoTabsRouter(
|
||||
routes: [
|
||||
const MainTimelineRoute(),
|
||||
SearchRoute(),
|
||||
const AlbumsRoute(),
|
||||
const LibraryRoute(),
|
||||
],
|
||||
duration: const Duration(milliseconds: 600),
|
||||
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
builder: (context, child) {
|
||||
final tabsRouter = AutoTabsRouter.of(context);
|
||||
final heroedChild = HeroControllerScope(
|
||||
controller: HeroController(),
|
||||
child: child,
|
||||
);
|
||||
return PopScope(
|
||||
canPop: tabsRouter.activeIndex == 0,
|
||||
onPopInvokedWithResult: (didPop, _) =>
|
||||
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: isScreenLandscape
|
||||
? Row(
|
||||
children: [
|
||||
navigationRail(tabsRouter),
|
||||
const VerticalDivider(),
|
||||
Expanded(child: heroedChild),
|
||||
],
|
||||
)
|
||||
: heroedChild,
|
||||
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
||||
? null
|
||||
: bottomNavigationBar(tabsRouter),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,9 @@ class EditImagePage extends ConsumerWidget {
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(7),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
@@ -135,7 +137,9 @@ class EditImagePage extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(7),
|
||||
),
|
||||
child: Image(
|
||||
image: image.image,
|
||||
fit: BoxFit.contain,
|
||||
@@ -149,7 +153,9 @@ class EditImagePage extends ConsumerWidget {
|
||||
margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: context.scaffoldBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(30),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
|
||||
@@ -162,13 +162,17 @@ class _FilterButton extends StatelessWidget {
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
border: isSelected
|
||||
? Border.all(color: context.primaryColor, width: 3)
|
||||
: null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10),
|
||||
),
|
||||
child: ColorFiltered(
|
||||
colorFilter: filter,
|
||||
child: FittedBox(
|
||||
|
||||
@@ -105,7 +105,9 @@ class QuickAccessButtons extends ConsumerWidget {
|
||||
color: context.colorScheme.onSurface.withAlpha(10),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(10),
|
||||
@@ -240,7 +242,9 @@ class PeopleCollectionCard extends ConsumerWidget {
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(30),
|
||||
|
||||
@@ -80,7 +80,9 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||
color: context.colorScheme.onSurface.withAlpha(10),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(10),
|
||||
|
||||
@@ -143,7 +143,9 @@ class PlaceTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
width: 80,
|
||||
height: 80,
|
||||
|
||||
@@ -156,7 +156,7 @@ class MapPage extends HookConsumerWidget {
|
||||
} else {
|
||||
// If no asset was previously selected and no new asset is available, close the bottom sheet
|
||||
if (selectedMarker.value == null) {
|
||||
bottomSheetStreamController.add(MapCloseBottomSheet());
|
||||
bottomSheetStreamController.add(const MapCloseBottomSheet());
|
||||
}
|
||||
selectedMarker.value = null;
|
||||
}
|
||||
|
||||
@@ -511,16 +511,11 @@ class SearchPage extends HookConsumerWidget {
|
||||
search();
|
||||
}
|
||||
|
||||
IconData getSearchPrefixIcon() {
|
||||
switch (textSearchType.value) {
|
||||
case TextSearchType.context:
|
||||
return Icons.image_search_rounded;
|
||||
case TextSearchType.filename:
|
||||
return Icons.abc_rounded;
|
||||
case TextSearchType.description:
|
||||
return Icons.text_snippet_outlined;
|
||||
}
|
||||
}
|
||||
IconData getSearchPrefixIcon() => switch (textSearchType.value) {
|
||||
TextSearchType.context => Icons.image_search_rounded,
|
||||
TextSearchType.filename => Icons.abc_rounded,
|
||||
TextSearchType.description => Icons.text_snippet_outlined,
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
@@ -533,8 +528,10 @@ class SearchPage extends HookConsumerWidget {
|
||||
style: MenuStyle(
|
||||
elevation: const WidgetStatePropertyAll(1),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(
|
||||
@@ -631,7 +628,9 @@ class SearchPage extends HookConsumerWidget {
|
||||
color: context.colorScheme.onSurface.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||
@@ -823,7 +822,9 @@ class QuickLinkList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.colorScheme.outline.withAlpha(10),
|
||||
width: 1,
|
||||
|
||||
@@ -91,7 +91,7 @@ final _features = [
|
||||
_Feature(
|
||||
name: 'Main Timeline',
|
||||
icon: Icons.timeline_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const MainTimelineRoute()),
|
||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||
final double width;
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
RemoteThumbProvider({
|
||||
const RemoteThumbProvider({
|
||||
required this.assetId,
|
||||
this.height = kTimelineFixedTileExtent,
|
||||
this.width = kTimelineFixedTileExtent,
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:thumbhash/thumbhash.dart';
|
||||
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
|
||||
final String thumbHash;
|
||||
|
||||
ThumbHashProvider({
|
||||
const ThumbHashProvider({
|
||||
required this.thumbHash,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class ThumbnailTile extends StatelessWidget {
|
||||
class ThumbnailTile extends ConsumerWidget {
|
||||
const ThumbnailTile(
|
||||
this.asset, {
|
||||
this.size = const Size.square(256),
|
||||
this.fit = BoxFit.cover,
|
||||
this.showStorageIndicator = true,
|
||||
this.canDeselect = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -16,46 +21,128 @@ class ThumbnailTile extends StatelessWidget {
|
||||
final BoxFit fit;
|
||||
final bool showStorageIndicator;
|
||||
|
||||
/// If we are allowed to deselect this image
|
||||
final bool canDeselect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetContainerColor = context.isDarkTheme
|
||||
? context.primaryColor.darken(amount: 0.6)
|
||||
: context.primaryColor.lighten(amount: 0.8);
|
||||
|
||||
final isSelected = ref
|
||||
.watch(multiSelectProvider.select((state) => state.selectedAssets))
|
||||
.contains(asset);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(child: Thumbnail(asset: asset, fit: fit, size: size)),
|
||||
if (asset.isVideo)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
|
||||
child: _VideoIndicator(asset.durationInSeconds ?? 0),
|
||||
AnimatedContainer(
|
||||
duration: Durations.short4,
|
||||
curve: Curves.decelerate,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (canDeselect ? assetContainerColor : Colors.grey)
|
||||
: null,
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: canDeselect ? assetContainerColor : Colors.grey,
|
||||
width: 8,
|
||||
)
|
||||
: const Border(),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: isSelected
|
||||
? const BorderRadius.all(Radius.circular(15.0))
|
||||
: BorderRadius.zero,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Thumbnail(
|
||||
asset: asset,
|
||||
fit: fit,
|
||||
size: size,
|
||||
),
|
||||
),
|
||||
if (asset.isVideo)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
|
||||
child: _VideoIndicator(asset.durationInSeconds ?? 0),
|
||||
),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(
|
||||
switch (asset.storage) {
|
||||
AssetState.local => Icons.cloud_off_outlined,
|
||||
AssetState.remote => Icons.cloud_outlined,
|
||||
AssetState.merged => Icons.cloud_done_outlined,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.favorite_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showStorageIndicator)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(
|
||||
switch (asset.storage) {
|
||||
AssetState.local => Icons.cloud_off_outlined,
|
||||
AssetState.remote => Icons.cloud_outlined,
|
||||
AssetState.merged => Icons.cloud_done_outlined,
|
||||
},
|
||||
),
|
||||
if (isSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: _SelectionIndicator(
|
||||
isSelected: isSelected,
|
||||
color: assetContainerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (asset.isFavorite)
|
||||
const Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
|
||||
child: _TileOverlayIcon(Icons.favorite_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectionIndicator extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final Color? color;
|
||||
const _SelectionIndicator({
|
||||
required this.isSelected,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isSelected) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Icon(
|
||||
Icons.circle_outlined,
|
||||
color: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoIndicator extends StatelessWidget {
|
||||
final int durationInSeconds;
|
||||
const _VideoIndicator(this.durationInSeconds);
|
||||
|
||||
@@ -9,7 +9,9 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class FixedSegment extends Segment {
|
||||
final double tileHeight;
|
||||
@@ -60,73 +62,169 @@ class FixedSegment extends Segment {
|
||||
return gridIndex + firstRowBelow - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget builder(BuildContext context, int index) {
|
||||
if (index == firstIndex) {
|
||||
return TimelineHeader(
|
||||
bucket: bucket,
|
||||
header: header,
|
||||
height: headerExtent,
|
||||
);
|
||||
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
|
||||
final multiSelectState = ref.read(multiSelectProvider);
|
||||
if (!multiSelectState.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||
}
|
||||
|
||||
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
||||
final multiSelectState = ref.read(multiSelectProvider);
|
||||
if (multiSelectState.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget builder(BuildContext context, int index) {
|
||||
final rowIndexInSegment = index - (firstIndex + 1);
|
||||
final assetIndex = rowIndexInSegment * columnCount;
|
||||
final assetCount = bucket.assetCount;
|
||||
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
|
||||
|
||||
if (index == firstIndex) {
|
||||
return TimelineHeader(
|
||||
bucket: bucket,
|
||||
header: header,
|
||||
height: headerExtent,
|
||||
assetOffset: firstAssetIndex,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
||||
}
|
||||
|
||||
Widget _buildRow(int assetIndex, int count) => Consumer(
|
||||
builder: (ctx, ref, _) {
|
||||
final isScrubbing =
|
||||
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
Widget _buildRow(int assetIndex, int count) => RepaintBoundary(
|
||||
child: Consumer(
|
||||
builder: (ctx, ref, _) {
|
||||
final isScrubbing =
|
||||
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
|
||||
// Timeline is being scrubbed, show placeholders
|
||||
if (isScrubbing) {
|
||||
return SegmentBuilder.buildPlaceholder(
|
||||
ctx,
|
||||
count,
|
||||
size: Size.square(tileHeight),
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
// Create stable callback references to prevent unnecessary rebuilds
|
||||
onTap(BaseAsset asset) => _handleOnTap(ref, asset);
|
||||
onLongPress(BaseAsset asset) => _handleOnLongPress(ref, asset);
|
||||
|
||||
// Bucket is already loaded, show the assets
|
||||
if (timelineService.hasRange(assetIndex, count)) {
|
||||
final assets = timelineService.getAssets(assetIndex, count);
|
||||
return _buildAssetRow(ctx, assets);
|
||||
}
|
||||
// Timeline is being scrubbed, show placeholders
|
||||
if (isScrubbing) {
|
||||
return SegmentBuilder.buildPlaceholder(
|
||||
ctx,
|
||||
count,
|
||||
size: Size.square(tileHeight),
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
// Bucket is not loaded, show placeholders and load the bucket
|
||||
return FutureBuilder(
|
||||
future: timelineService.loadAssets(assetIndex, count),
|
||||
builder: (ctxx, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return SegmentBuilder.buildPlaceholder(
|
||||
ctx,
|
||||
count,
|
||||
size: Size.square(tileHeight),
|
||||
spacing: spacing,
|
||||
// Bucket is already loaded, show the assets
|
||||
if (timelineService.hasRange(assetIndex, count)) {
|
||||
final assets = timelineService.getAssets(assetIndex, count);
|
||||
return _buildAssetRow(
|
||||
ctx,
|
||||
assets,
|
||||
baseAssetIndex: assetIndex,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
|
||||
// Bucket is not loaded, show placeholders and load the bucket
|
||||
return FutureBuilder(
|
||||
future: timelineService.loadAssets(assetIndex, count),
|
||||
builder: (ctxx, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return SegmentBuilder.buildPlaceholder(
|
||||
ctx,
|
||||
count,
|
||||
size: Size.square(tileHeight),
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildAssetRow(
|
||||
ctxx,
|
||||
snap.requireData,
|
||||
baseAssetIndex: assetIndex,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildAssetRow(ctxx, snap.requireData);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) =>
|
||||
Widget _buildAssetRow(
|
||||
BuildContext context,
|
||||
List<BaseAsset> assets, {
|
||||
required void Function(BaseAsset) onTap,
|
||||
required void Function(BaseAsset) onLongPress,
|
||||
required int baseAssetIndex,
|
||||
}) =>
|
||||
FixedTimelineRow(
|
||||
dimension: tileHeight,
|
||||
spacing: spacing,
|
||||
textDirection: Directionality.of(context),
|
||||
children: List.generate(
|
||||
assets.length,
|
||||
(i) => RepaintBoundary(child: ThumbnailTile(assets[i])),
|
||||
(i) => _AssetTileWidget(
|
||||
key: ValueKey(_generateUniqueKey(assets[i], baseAssetIndex + i)),
|
||||
asset: assets[i],
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Generates a unique key for an asset that handles different asset types
|
||||
/// and prevents duplicate keys even when assets have the same name/timestamp
|
||||
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
|
||||
// Try to get the most unique identifier based on asset type
|
||||
if (asset is Asset) {
|
||||
// For remote/merged assets, use the remote ID which is globally unique
|
||||
return 'asset_${asset.id}';
|
||||
} else if (asset is LocalAsset) {
|
||||
// For local assets, use the local ID which should be unique per device
|
||||
return 'local_${asset.id}';
|
||||
} else {
|
||||
// Fallback for any other BaseAsset implementation
|
||||
// Use checksum if available for additional uniqueness
|
||||
final checksum = asset.checksum;
|
||||
if (checksum != null && checksum.isNotEmpty) {
|
||||
return 'checksum_${checksum.hashCode}';
|
||||
} else {
|
||||
// Last resort: use global asset index + object hash for uniqueness
|
||||
return 'fallback_${assetIndex}_${asset.hashCode}_${asset.createdAt.microsecondsSinceEpoch}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetTileWidget extends StatelessWidget {
|
||||
final BaseAsset asset;
|
||||
final void Function(BaseAsset) onTap;
|
||||
final void Function(BaseAsset) onLongPress;
|
||||
|
||||
const _AssetTileWidget({
|
||||
super.key,
|
||||
required this.asset,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(asset),
|
||||
onLongPress: () => onLongPress(asset),
|
||||
child: ThumbnailTile(asset),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class TimelineHeader extends StatelessWidget {
|
||||
class TimelineHeader extends ConsumerWidget {
|
||||
final Bucket bucket;
|
||||
final HeaderType header;
|
||||
final double height;
|
||||
final int assetOffset;
|
||||
|
||||
const TimelineHeader({
|
||||
super.key,
|
||||
required this.bucket,
|
||||
required this.header,
|
||||
required this.height,
|
||||
required this.assetOffset,
|
||||
});
|
||||
|
||||
String _formatMonth(BuildContext context, DateTime date) {
|
||||
@@ -28,33 +36,118 @@ class TimelineHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (bucket is! TimeBucket || header == HeaderType.none) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final date = (bucket as TimeBucket).date;
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 10, top: 30, bottom: 10),
|
||||
height: height,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
if (header == HeaderType.month || header == HeaderType.monthAndDay)
|
||||
Text(
|
||||
_formatMonth(context, date),
|
||||
style: context.textTheme.labelLarge
|
||||
?.copyWith(fontSize: 24, fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (header == HeaderType.day || header == HeaderType.monthAndDay)
|
||||
Text(
|
||||
_formatDay(context, date),
|
||||
style: context.textTheme.labelLarge
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
|
||||
List<BaseAsset> bucketAssets;
|
||||
try {
|
||||
bucketAssets = ref
|
||||
.watch(timelineServiceProvider)
|
||||
.getAssets(assetOffset, bucket.assetCount);
|
||||
} catch (e) {
|
||||
bucketAssets = <BaseAsset>[];
|
||||
}
|
||||
|
||||
final isAllSelected = ref.watch(bucketSelectionProvider(bucketAssets));
|
||||
final isMonthHeader =
|
||||
header == HeaderType.month || header == HeaderType.monthAndDay;
|
||||
final isDayHeader =
|
||||
header == HeaderType.day || header == HeaderType.monthAndDay;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: isMonthHeader ? 8.0 : 0.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (isMonthHeader)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
_formatMonth(context, date),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 24),
|
||||
),
|
||||
const Spacer(),
|
||||
if (header != HeaderType.monthAndDay)
|
||||
_BulkSelectIconButton(
|
||||
isAllSelected: isAllSelected,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(multiSelectProvider.notifier)
|
||||
.toggleBucketSelection(
|
||||
assetOffset,
|
||||
bucket.assetCount,
|
||||
);
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDayHeader)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
_formatDay(context, date),
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_BulkSelectIconButton(
|
||||
isAllSelected: isAllSelected,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(multiSelectProvider.notifier)
|
||||
.toggleBucketSelection(
|
||||
assetOffset,
|
||||
bucket.assetCount,
|
||||
);
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BulkSelectIconButton extends ConsumerWidget {
|
||||
final bool isAllSelected;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _BulkSelectIconButton({
|
||||
required this.isAllSelected,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: isAllSelected
|
||||
? Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 26,
|
||||
color: context.primaryColor,
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
size: 26,
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +44,16 @@ List<_Segment> _buildSegments({
|
||||
required List<Segment> layoutSegments,
|
||||
required double timelineHeight,
|
||||
}) {
|
||||
const double offsetThreshold = 20.0;
|
||||
|
||||
final segments = <_Segment>[];
|
||||
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final formatter = DateFormat.yMMM();
|
||||
DateTime? lastDate;
|
||||
double lastOffset = -offsetThreshold;
|
||||
for (final layoutSegment in layoutSegments) {
|
||||
final scrollPercentage =
|
||||
layoutSegment.startOffset / layoutSegments.last.endOffset;
|
||||
@@ -58,13 +62,21 @@ List<_Segment> _buildSegments({
|
||||
final date = (layoutSegment.bucket as TimeBucket).date;
|
||||
final label = formatter.format(date);
|
||||
|
||||
final showSegment = lastOffset + offsetThreshold <= startOffset &&
|
||||
(lastDate == null || date.year != lastDate.year);
|
||||
|
||||
segments.add(
|
||||
_Segment(
|
||||
date: date,
|
||||
startOffset: startOffset,
|
||||
scrollLabel: label,
|
||||
showSegment: showSegment,
|
||||
),
|
||||
);
|
||||
lastDate = date;
|
||||
if (showSegment) {
|
||||
lastOffset = startOffset;
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
@@ -85,12 +97,15 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
||||
double get _scrubberHeight =>
|
||||
widget.timelineHeight - widget.topPadding - widget.bottomPadding;
|
||||
|
||||
late final ScrollController _scrollController;
|
||||
late ScrollController _scrollController;
|
||||
|
||||
double get _currentOffset =>
|
||||
_scrollController.offset *
|
||||
_scrubberHeight /
|
||||
_scrollController.position.maxScrollExtent;
|
||||
double get _currentOffset {
|
||||
if (_scrollController.hasClients != true) return 0.0;
|
||||
|
||||
return _scrollController.offset *
|
||||
_scrubberHeight /
|
||||
_scrollController.position.maxScrollExtent;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -194,28 +209,102 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
||||
_thumbAnimationController.forward();
|
||||
}
|
||||
|
||||
final newOffset =
|
||||
details.globalPosition.dy - widget.topPadding - widget.bottomPadding;
|
||||
final dragPosition = _calculateDragPosition(details);
|
||||
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
|
||||
|
||||
if (nearestMonthSegment != null) {
|
||||
_snapToSegment(nearestMonthSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the drag position relative to the scrubber area
|
||||
///
|
||||
/// This method converts the global drag coordinates from the gesture detector
|
||||
/// into a position relative to the scrubber's active area (excluding padding).
|
||||
///
|
||||
/// The scrubber has padding at the top and bottom, so we need to:
|
||||
/// 1. Calculate the actual draggable area (timelineHeight - topPadding - bottomPadding)
|
||||
/// 2. Convert the global Y position to a position within this draggable area
|
||||
/// 3. Clamp the result to ensure it stays within bounds (0 to dragAreaHeight)
|
||||
///
|
||||
/// Example:
|
||||
/// - If timelineHeight = 800, topPadding = 50, bottomPadding = 50
|
||||
/// - Then dragAreaHeight = 700 (the actual scrubber area)
|
||||
/// - If user drags to global Y position that's 100 pixels from the top
|
||||
/// - The relative position would be 100 - 50 = 50 (50 pixels into the scrubber area)
|
||||
double _calculateDragPosition(DragUpdateDetails details) {
|
||||
final dragAreaTop = widget.topPadding;
|
||||
final dragAreaBottom = widget.timelineHeight - widget.bottomPadding;
|
||||
final dragAreaHeight = dragAreaBottom - dragAreaTop;
|
||||
|
||||
final relativePosition = details.globalPosition.dy - dragAreaTop;
|
||||
|
||||
// Make sure the position stays within the scrubber's bounds
|
||||
return relativePosition.clamp(0.0, dragAreaHeight);
|
||||
}
|
||||
|
||||
/// Find the segment closest to the given position
|
||||
_Segment? _findNearestMonthSegment(double position) {
|
||||
_Segment? nearestSegment;
|
||||
double minDistance = double.infinity;
|
||||
|
||||
for (final segment in _segments) {
|
||||
final distance = (segment.startOffset - position).abs();
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestSegment = segment;
|
||||
}
|
||||
}
|
||||
|
||||
return nearestSegment;
|
||||
}
|
||||
|
||||
/// Snap the scrubber thumb and scroll view to the given segment
|
||||
void _snapToSegment(_Segment segment) {
|
||||
setState(() {
|
||||
_thumbTopOffset = newOffset.clamp(0, _scrubberHeight);
|
||||
final scrollPercentage = _thumbTopOffset / _scrubberHeight;
|
||||
final maxScrollExtent = _scrollController.position.maxScrollExtent;
|
||||
_scrollController.jumpTo(maxScrollExtent * scrollPercentage);
|
||||
_thumbTopOffset = segment.startOffset;
|
||||
|
||||
final layoutSegmentIndex = _findLayoutSegmentIndex(segment);
|
||||
|
||||
if (layoutSegmentIndex >= 0) {
|
||||
_scrollToLayoutSegment(layoutSegmentIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int _findLayoutSegmentIndex(_Segment segment) {
|
||||
return widget.layoutSegments.indexWhere(
|
||||
(layoutSegment) {
|
||||
final bucket = layoutSegment.bucket as TimeBucket;
|
||||
return bucket.date.year == segment.date.year &&
|
||||
bucket.date.month == segment.date.month;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _scrollToLayoutSegment(int layoutSegmentIndex) {
|
||||
final layoutSegment = widget.layoutSegments[layoutSegmentIndex];
|
||||
final maxScrollExtent = _scrollController.position.maxScrollExtent;
|
||||
final viewportHeight = _scrollController.position.viewportDimension;
|
||||
|
||||
final targetScrollOffset = layoutSegment.startOffset;
|
||||
final centeredOffset = targetScrollOffset - (viewportHeight / 4) + 100;
|
||||
|
||||
_scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent));
|
||||
}
|
||||
|
||||
void _onDragEnd(WidgetRef ref) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||
_labelAnimationController.reverse();
|
||||
_isDragging = false;
|
||||
|
||||
_resetThumbTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) {
|
||||
Text? label;
|
||||
if (_scrollController.hasClients) {
|
||||
if (_scrollController.hasClients == true) {
|
||||
// Cache to avoid multiple calls to [_currentOffset]
|
||||
final scrollOffset = _currentOffset;
|
||||
final labelText = _segments
|
||||
@@ -240,20 +329,31 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
||||
child: Stack(
|
||||
children: [
|
||||
RepaintBoundary(child: widget.child),
|
||||
// Scroll Segments - wrapped in RepaintBoundary for better performance
|
||||
RepaintBoundary(
|
||||
child: _SegmentsLayer(
|
||||
key: ValueKey('segments_${_isDragging}_${_segments.length}'),
|
||||
segments: _segments,
|
||||
topPadding: widget.topPadding,
|
||||
isDragging: _isDragging,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
top: _thumbTopOffset + widget.topPadding,
|
||||
end: 0,
|
||||
child: Consumer(
|
||||
builder: (_, ref, child) => GestureDetector(
|
||||
onVerticalDragStart: (_) => _onDragStart(ref),
|
||||
onVerticalDragUpdate: _onDragUpdate,
|
||||
onVerticalDragEnd: (_) => _onDragEnd(ref),
|
||||
child: child,
|
||||
),
|
||||
child: _Scrubber(
|
||||
thumbAnimation: _thumbAnimation,
|
||||
labelAnimation: _labelAnimation,
|
||||
label: label,
|
||||
child: RepaintBoundary(
|
||||
child: Consumer(
|
||||
builder: (_, ref, child) => GestureDetector(
|
||||
onVerticalDragStart: (_) => _onDragStart(ref),
|
||||
onVerticalDragUpdate: _onDragUpdate,
|
||||
onVerticalDragEnd: (_) => _onDragEnd(ref),
|
||||
child: child,
|
||||
),
|
||||
child: _Scrubber(
|
||||
thumbAnimation: _thumbAnimation,
|
||||
labelAnimation: _labelAnimation,
|
||||
label: label,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -263,6 +363,72 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
class _SegmentsLayer extends StatelessWidget {
|
||||
final List<_Segment> segments;
|
||||
final double topPadding;
|
||||
final bool isDragging;
|
||||
|
||||
const _SegmentsLayer({
|
||||
super.key,
|
||||
required this.segments,
|
||||
required this.topPadding,
|
||||
required this.isDragging,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Visibility(
|
||||
visible: isDragging,
|
||||
child: Stack(
|
||||
children: segments
|
||||
.where((segment) => segment.showSegment)
|
||||
.map(
|
||||
(segment) => PositionedDirectional(
|
||||
key: ValueKey('segment_${segment.date.millisecondsSinceEpoch}'),
|
||||
top: topPadding + segment.startOffset,
|
||||
end: 100,
|
||||
child: RepaintBoundary(
|
||||
child: _SegmentWidget(segment),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SegmentWidget extends StatelessWidget {
|
||||
final _Segment _segment;
|
||||
|
||||
const _SegmentWidget(this._segment);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 12.0),
|
||||
child: Material(
|
||||
color: context.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 28),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_segment.date.year.toString(),
|
||||
style: context.textTheme.labelMedium?.copyWith(
|
||||
fontFamily: "OverpassMono",
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScrollLabel extends StatelessWidget {
|
||||
final Text label;
|
||||
final Color backgroundColor;
|
||||
@@ -429,22 +595,26 @@ class _Segment {
|
||||
final DateTime date;
|
||||
final double startOffset;
|
||||
final String scrollLabel;
|
||||
final bool showSegment;
|
||||
|
||||
const _Segment({
|
||||
required this.date,
|
||||
required this.startOffset,
|
||||
required this.scrollLabel,
|
||||
this.showSegment = false,
|
||||
});
|
||||
|
||||
_Segment copyWith({
|
||||
DateTime? date,
|
||||
double? startOffset,
|
||||
String? scrollLabel,
|
||||
bool? showSegment,
|
||||
}) {
|
||||
return _Segment(
|
||||
date: date ?? this.date,
|
||||
startOffset: startOffset ?? this.startOffset,
|
||||
scrollLabel: scrollLabel ?? this.scrollLabel,
|
||||
showSegment: showSegment ?? this.showSegment,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,18 +15,12 @@ abstract class SegmentBuilder {
|
||||
this.groupBy = GroupAssetsBy.day,
|
||||
});
|
||||
|
||||
static double headerExtent(HeaderType header) {
|
||||
switch (header) {
|
||||
case HeaderType.month:
|
||||
return kTimelineHeaderExtent;
|
||||
case HeaderType.day:
|
||||
return kTimelineHeaderExtent * 0.90;
|
||||
case HeaderType.monthAndDay:
|
||||
return kTimelineHeaderExtent * 1.5;
|
||||
case HeaderType.none:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
static double headerExtent(HeaderType header) => switch (header) {
|
||||
HeaderType.month => kTimelineHeaderExtent,
|
||||
HeaderType.day => kTimelineHeaderExtent * 0.90,
|
||||
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6,
|
||||
HeaderType.none => 0.0,
|
||||
};
|
||||
|
||||
static Widget buildPlaceholder(
|
||||
BuildContext context,
|
||||
|
||||
@@ -13,6 +13,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||
|
||||
class Timeline extends StatelessWidget {
|
||||
const Timeline({super.key});
|
||||
@@ -63,38 +65,68 @@ class _SliverTimelineState extends State<_SliverTimeline> {
|
||||
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||
final maxHeight =
|
||||
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
||||
final isMultiSelectEnabled =
|
||||
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||
return asyncSegments.widgetWhen(
|
||||
onData: (segments) {
|
||||
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
||||
final statusBarHeight = context.padding.top;
|
||||
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
|
||||
const scrubberBottomPadding = 100.0;
|
||||
|
||||
return PrimaryScrollController(
|
||||
controller: _scrollController,
|
||||
child: Scrubber(
|
||||
layoutSegments: segments,
|
||||
timelineHeight: maxHeight,
|
||||
topPadding: context.padding.top + 10,
|
||||
bottomPadding: context.padding.bottom + 10,
|
||||
child: CustomScrollView(
|
||||
primary: true,
|
||||
cacheExtent: maxHeight * 2,
|
||||
slivers: [
|
||||
_SliverSegmentedList(
|
||||
segments: segments,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(ctx, index) {
|
||||
if (index >= childCount) return null;
|
||||
final segment = segments.findByIndex(index);
|
||||
return segment?.builder(ctx, index) ??
|
||||
const SizedBox.shrink();
|
||||
},
|
||||
childCount: childCount,
|
||||
addAutomaticKeepAlives: false,
|
||||
// We add repaint boundary around tiles, so skip the auto boundaries
|
||||
addRepaintBoundaries: false,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Scrubber(
|
||||
layoutSegments: segments,
|
||||
timelineHeight: maxHeight,
|
||||
topPadding: totalAppBarHeight + 10,
|
||||
bottomPadding:
|
||||
context.padding.bottom + scrubberBottomPadding,
|
||||
child: CustomScrollView(
|
||||
primary: true,
|
||||
cacheExtent: maxHeight * 2,
|
||||
slivers: [
|
||||
SliverAnimatedOpacity(
|
||||
duration: Durations.medium1,
|
||||
opacity: isMultiSelectEnabled ? 0 : 1,
|
||||
sliver: const ImmichSliverAppBar(
|
||||
floating: true,
|
||||
pinned: false,
|
||||
snap: false,
|
||||
),
|
||||
),
|
||||
_SliverSegmentedList(
|
||||
segments: segments,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(ctx, index) {
|
||||
if (index >= childCount) return null;
|
||||
final segment = segments.findByIndex(index);
|
||||
return segment?.builder(ctx, index) ??
|
||||
const SizedBox.shrink();
|
||||
},
|
||||
childCount: childCount,
|
||||
addAutomaticKeepAlives: false,
|
||||
// We add repaint boundary around tiles, so skip the auto boundaries
|
||||
addRepaintBoundaries: false,
|
||||
),
|
||||
),
|
||||
const SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: scrubberBottomPadding,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isMultiSelectEnabled)
|
||||
const Positioned(
|
||||
top: 60,
|
||||
left: 25,
|
||||
child: _MultiSelectStatusButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -363,3 +395,27 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
||||
childManager.didFinishLayout();
|
||||
}
|
||||
}
|
||||
|
||||
class _MultiSelectStatusButton extends ConsumerWidget {
|
||||
const _MultiSelectStatusButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectCount =
|
||||
ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
|
||||
return ElevatedButton.icon(
|
||||
onPressed: () => ref.read(multiSelectProvider.notifier).clearSelection(),
|
||||
icon: Icon(
|
||||
Icons.close_rounded,
|
||||
color: context.colorScheme.onPrimary,
|
||||
),
|
||||
label: Text(
|
||||
selectCount.toString(),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
height: 2.5,
|
||||
color: context.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
|
||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||
AlbumViewerNotifier(this.ref)
|
||||
: super(
|
||||
AlbumViewerPageState(
|
||||
const AlbumViewerPageState(
|
||||
editTitleText: "",
|
||||
isEditAlbum: false,
|
||||
editDescriptionText: "",
|
||||
|
||||
@@ -5,4 +5,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'app_settings.provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppSettingsService appSettingsService(Ref _) => AppSettingsService();
|
||||
AppSettingsService appSettingsService(Ref _) => const AppSettingsService();
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsServiceHash() =>
|
||||
r'2aa16d76a8df869c39486325efc1d08b2d2c284c';
|
||||
r'89cece3a19e06612f5639ae290120e854a0c5a31';
|
||||
|
||||
/// See also [appSettingsService].
|
||||
@ProviderFor(appSettingsService)
|
||||
|
||||
@@ -23,7 +23,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
||||
this._shareService,
|
||||
this._albumService,
|
||||
) : super(
|
||||
DownloadState(
|
||||
const DownloadState(
|
||||
downloadStatus: TaskStatus.complete,
|
||||
showProgress: false,
|
||||
taskProgress: <String, DownloadInfo>{},
|
||||
|
||||
@@ -45,7 +45,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
this._secureStorageService,
|
||||
this._widgetService,
|
||||
) : super(
|
||||
AuthState(
|
||||
const AuthState(
|
||||
deviceId: "",
|
||||
userId: "",
|
||||
userEmail: "",
|
||||
@@ -89,7 +89,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}
|
||||
|
||||
Future<void> _cleanUp() async {
|
||||
state = AuthState(
|
||||
state = const AuthState(
|
||||
deviceId: "",
|
||||
userId: "",
|
||||
userEmail: "",
|
||||
@@ -118,8 +118,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}) async {
|
||||
await _apiService.setAccessToken(accessToken);
|
||||
|
||||
await _widgetService.writeSessionKey(accessToken);
|
||||
await _widgetService.writeServerList();
|
||||
await _widgetService.writeCredentials(
|
||||
Store.get(StoreKey.serverEndpoint),
|
||||
accessToken,
|
||||
);
|
||||
|
||||
// Get the deviceid from the store if it exists, otherwise generate a new one
|
||||
String deviceId =
|
||||
@@ -188,7 +190,6 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
Future<void> saveLocalEndpoint(String url) async {
|
||||
await Store.put(StoreKey.localEndpoint, url);
|
||||
await _widgetService.writeServerList();
|
||||
}
|
||||
|
||||
String? getSavedWifiName() {
|
||||
|
||||
@@ -7,7 +7,7 @@ class IOSBackgroundSettings {
|
||||
final DateTime? timeOfLastFetch;
|
||||
final DateTime? timeOfLastProcessing;
|
||||
|
||||
IOSBackgroundSettings({
|
||||
const IOSBackgroundSettings({
|
||||
required this.appRefreshEnabled,
|
||||
required this.numberOfBackgroundTasksQueued,
|
||||
this.timeOfLastFetch,
|
||||
|
||||
@@ -42,6 +42,6 @@ class ImageLoader {
|
||||
}
|
||||
|
||||
// If we get here, the image failed to load from the cache stream
|
||||
throw ImageLoadingException('Could not load image from stream');
|
||||
throw const ImageLoadingException('Could not load image from stream');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// An exception for the [ImageLoader] and the Immich image providers
|
||||
class ImageLoadingException implements Exception {
|
||||
final String message;
|
||||
ImageLoadingException(this.message);
|
||||
const ImageLoadingException(this.message);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ImmichRemoteImageProvider
|
||||
/// The image cache manager
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
ImmichRemoteImageProvider({
|
||||
const ImmichRemoteImageProvider({
|
||||
required this.assetId,
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ class ImmichRemoteThumbnailProvider
|
||||
/// The image cache manager
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
ImmichRemoteThumbnailProvider({
|
||||
const ImmichRemoteThumbnailProvider({
|
||||
required this.assetId,
|
||||
this.height,
|
||||
this.width,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
final inLockedViewProvider = StateProvider<bool>((ref) => false);
|
||||
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
@@ -17,7 +17,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
||||
final SearchService _searchService;
|
||||
|
||||
PaginatedSearchNotifier(this._searchService)
|
||||
: super(SearchResult(assets: [], nextPage: 1));
|
||||
: super(const SearchResult(assets: [], nextPage: 1));
|
||||
|
||||
Future<bool> search(SearchFilter filter) async {
|
||||
if (state.nextPage == null) {
|
||||
@@ -39,7 +39,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
||||
}
|
||||
|
||||
clear() {
|
||||
state = SearchResult(assets: [], nextPage: 1);
|
||||
state = const SearchResult(assets: [], nextPage: 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||
|
||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
ServerInfoNotifier(this._serverInfoService)
|
||||
: super(
|
||||
ServerInfo(
|
||||
serverVersion: const ServerVersion(
|
||||
const ServerInfo(
|
||||
serverVersion: ServerVersion(
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
),
|
||||
latestVersion: const ServerVersion(
|
||||
latestVersion: ServerVersion(
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
),
|
||||
serverFeatures: const ServerFeatures(
|
||||
serverFeatures: ServerFeatures(
|
||||
map: true,
|
||||
trash: true,
|
||||
oauthEnabled: false,
|
||||
passwordLogin: true,
|
||||
),
|
||||
serverConfig: const ServerConfig(
|
||||
serverConfig: ServerConfig(
|
||||
trashDays: 30,
|
||||
oauthButtonText: '',
|
||||
externalDomain: '',
|
||||
@@ -38,7 +37,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||
'https://tiles.immich.cloud/v1/style/light.json',
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
),
|
||||
serverDiskInfo: const ServerDiskInfo(
|
||||
serverDiskInfo: ServerDiskInfo(
|
||||
diskAvailable: "0",
|
||||
diskSize: "0",
|
||||
diskUse: "0",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user