Compare commits

..

1 Commits

Author SHA1 Message Date
Ben McCann
345e14921c chore: convert code projects to use pnpm 2025-06-24 12:44:57 -07:00
179 changed files with 15769 additions and 42402 deletions

View File

@@ -73,7 +73,7 @@ jobs:
cache: 'gradle' cache: 'gradle'
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml

View File

@@ -33,21 +33,22 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Prepare SDK - name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/ run: pnpm install --frozen-lockfile
- name: Build SDK - name: Build SDK
run: npm run build --prefix ../open-api/typescript-sdk/ run: pnpm --dir ../open-api/typescript-sdk/ build
- run: npm ci - run: pnpm build
- run: npm run build - run: pnpm publish
- run: npm publish
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -70,7 +71,7 @@ jobs:
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
@@ -99,7 +100,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
# Command-line programs to run using the OS shell. # 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 # 📚 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 # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -177,7 +177,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}
@@ -188,6 +188,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}

View File

@@ -28,12 +28,14 @@ jobs:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Fix formatting - name: Fix formatting
run: make install-all && make format-all run: make install-all && make format-all

View File

@@ -100,7 +100,7 @@ jobs:
name: release-apk-signed name: release-apk-signed
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
with: with:
draft: true draft: true
tag_name: ${{ env.IMMICH_VERSION }} tag_name: ${{ env.IMMICH_VERSION }}

View File

@@ -20,18 +20,20 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './open-api/typescript-sdk/.nvmrc' node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Install deps - name: Install deps
run: npm ci run: pnpm install --frozen-lockfile
- name: Build - name: Build
run: npm run build run: pnpm build
- name: Publish - name: Publish
run: npm publish run: pnpm publish
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -49,7 +49,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
@@ -109,7 +109,7 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
- name: Run DCM - name: Run DCM
run: dcm analyze lib --fatal-style --fatal-warnings run: dcm analyze lib
working-directory: ./mobile working-directory: ./mobile
zizmor: zizmor:
@@ -134,7 +134,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0 uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -80,30 +80,32 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run pnpm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run linter - name: Run linter
run: npm run lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run small tests & coverage - name: Run small tests & coverage
run: npm test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
cli-unit-tests: cli-unit-tests:
@@ -123,34 +125,36 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Setup typescript-sdk - name: Setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Install deps
run: npm ci run: pnpm install --frozen-lockfile
- name: Run linter - name: Run linter
run: npm run lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
cli-unit-tests-win: cli-unit-tests-win:
@@ -170,27 +174,29 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Setup typescript-sdk - name: Setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Install deps - name: Install deps
run: npm ci run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test. # Skip linter & formatter in Windows test.
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-lint: web-lint:
@@ -210,30 +216,32 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Run npm install - name: Run pnpm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run linter - name: Run linter
run: npm run lint:p run: pnpm lint:p
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run svelte checks - name: Run svelte checks
run: npm run check:svelte run: pnpm check:svelte
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
web-unit-tests: web-unit-tests:
@@ -253,26 +261,28 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
- name: Run npm install - name: Run pnpm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run tsc - name: Run tsc
run: npm run check:typescript run: pnpm run check:typescript
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run unit tests & coverage - name: Run unit tests & coverage
run: npm run test run: pnpm run test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
i18n-tests: i18n-tests:
@@ -288,18 +298,20 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies - name: Install dependencies
run: npm --prefix=web ci run: pnpm install --frozen-lockfile
- name: Format - name: Format
run: npm --prefix=web run format:i18n run: pnpm --dir=web format:i18n
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -334,32 +346,34 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm run build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run linter - name: Run linter
run: npm run lint run: pnpm lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run tsc - name: Run tsc
run: npm run check run: pnpm check
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
server-medium-tests: server-medium-tests:
@@ -379,18 +393,20 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run pnpm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run medium tests - name: Run medium tests
run: npm run test:medium run: pnpm test:medium
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-server-cli: e2e-tests-server-cli:
@@ -414,25 +430,27 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run setup cli - name: Run setup cli
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli working-directory: ./cli
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Docker build - name: Docker build
@@ -440,7 +458,7 @@ jobs:
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli) - name: Run e2e tests (api & cli)
run: npm run test run: pnpm test
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
e2e-tests-web: e2e-tests-web:
@@ -464,20 +482,22 @@ jobs:
persist-credentials: false persist-credentials: false
submodules: 'recursive' submodules: 'recursive'
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
@@ -499,7 +519,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}
@@ -516,7 +536,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0 uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
with: with:
channel: 'stable' channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml flutter-version-file: ./mobile/pubspec.yaml
@@ -584,18 +604,20 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './.github/.nvmrc' node-version-file: './.github/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Run npm install - name: Run pnpm install
run: npm ci run: pnpm install --frozen-lockfile
- name: Run formatter - name: Run formatter
run: npm run format run: pnpm format
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
shellcheck: shellcheck:
@@ -627,18 +649,20 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Install server dependencies - name: Install server dependencies
run: npm --prefix=server ci run: pnpm install --frozen-lockfile
- name: Build the app - name: Build the app
run: npm --prefix=server run build run: pnpm --dir=server build
- name: Run API generation - name: Run API generation
run: make open-api run: make open-api
@@ -668,7 +692,7 @@ jobs:
contents: read contents: read
services: services:
postgres: postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
@@ -690,28 +714,30 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'npm' cache: 'pnpm'
cache-dependency-path: '**/package-lock.json'
- name: Install server dependencies - name: Install server dependencies
run: npm ci run: pnpm install --frozen-lockfile
- name: Build the app - name: Build the app
run: npm run build run: pnpm build
- name: Run existing migrations - name: Run existing migrations
run: npm run migrations:run run: pnpm migrations:run
- name: Test npm run schema:reset command works - name: Test pnpm schema:reset command works
run: npm run schema:reset run: pnpm schema:reset
- name: Generate new migrations - name: Generate new migrations
continue-on-error: true continue-on-error: true
run: npm run migrations:generate src/TestMigration run: pnpm migrations:generate src/TestMigration
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -730,7 +756,7 @@ jobs:
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: npm run sync:sql run: pnpm sync:sql
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich

View File

@@ -52,6 +52,6 @@ jobs:
permissions: {} permissions: {}
if: always() if: always()
steps: steps:
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4 - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
with: with:
needs: ${{ toJSON(needs) }} needs: ${{ toJSON(needs) }}

View File

@@ -34,7 +34,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql: sql:
npm --prefix server run sync:sql pnpm --dir server run sync:sql
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh
@@ -45,30 +45,30 @@ renovate:
MODULES = e2e server web cli sdk docs .github MODULES = e2e server web cli sdk docs .github
audit-%: audit-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix pnpm --dir $(subst sdk,open-api/typescript-sdk,$*) audit fix
install-%: install-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i pnpm --dir $(subst sdk,open-api/typescript-sdk,$*) i
ci-%: ci-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) ci pnpm --dir $(subst sdk,open-api/typescript-sdk,$*) install --frozen-lockfile
build-cli: build-sdk build-cli: build-sdk
build-web: build-sdk build-web: build-sdk
build-%: install-% build-%: install-%
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build pnpm --dir $(subst sdk,open-api/typescript-sdk,$*) build
format-%: format-%:
npm --prefix $* run format:fix pnpm --dir $* format:fix
lint-%: lint-%:
npm --prefix $* run lint:fix pnpm --dir $* lint:fix
check-%: check-%:
npm --prefix $* run check pnpm --dir $* check
check-web: check-web:
npm --prefix web run check:typescript pnpm --dir web check:typescript
npm --prefix web run check:svelte pnpm --dir web check:svelte
test-%: test-%:
npm --prefix $* run test pnpm --dir $* test
test-e2e: test-e2e:
docker compose -f ./e2e/docker-compose.yml build docker compose -f ./e2e/docker-compose.yml build
npm --prefix e2e run test pnpm --dir e2e test
npm --prefix e2e run test:web pnpm --dir e2e test:web
test-medium: test-medium:
docker run \ docker run \
--rm \ --rm \
@@ -78,9 +78,9 @@ test-medium:
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \ -v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
-e NODE_ENV=development \ -e NODE_ENV=development \
immich-server:latest \ immich-server:latest \
-c "npm ci && npm run test:medium -- --run" -c "pnpm install --frozen-lockfile && pnpm test:medium -- --run"
test-medium-dev: test-medium-dev:
docker exec -it immich_server /bin/sh -c "npm run test:medium" docker exec -it immich_server /bin/sh -c "pnpm test:medium"
build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ; build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
install-all: $(foreach M,$(filter-out .github,$(MODULES)),install-$M) ; install-all: $(foreach M,$(filter-out .github,$(MODULES)),install-$M) ;

View File

@@ -9,7 +9,6 @@ upload/**
.prettierignore .prettierignore
.prettierrc .prettierrc
Dockerfile Dockerfile
package-lock.json
tsconfig.json tsconfig.json
vite.config.ts vite.config.ts
vitest.config.ts vitest.config.ts

View File

@@ -2,17 +2,17 @@ FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
RUN npm ci RUN pnpm install --frozen-lockfile
COPY open-api/typescript-sdk/ ./ COPY open-api/typescript-sdk/ ./
RUN npm run build RUN pnpm build
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY cli/package.json cli/package-lock.json ./ COPY cli/package.json pnpm-lock.yaml ./
RUN npm ci RUN pnpm install --frozen-lockfile
COPY cli . COPY cli .
RUN npm run build RUN pnpm build
WORKDIR /import WORKDIR /import

View File

@@ -6,8 +6,8 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder: Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
$ npm install $ pnpm install
$ npm run build $ pnpm build
Then, to build the open-api client run the following in the open-api folder: Then, to build the open-api client run the following in the open-api folder:
@@ -15,8 +15,8 @@ Then, to build the open-api client run the following in the open-api folder:
To run the Immich CLI from source, run the following in the cli folder: To run the Immich CLI from source, run the following in the cli folder:
$ npm install $ pnpm install
$ npm run build $ pnpm build
$ ts-node . $ ts-node .
You'll need ts-node, the easiest way to install it is to use npm: You'll need ts-node, the easiest way to install it is to use npm:
@@ -25,6 +25,6 @@ You'll need ts-node, the easiest way to install it is to use npm:
You can also build and install the CLI using You can also build and install the CLI using
$ npm run build $ pnpm build
$ npm install -g . $ pnpm install -g .
**** ****

4517
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0", "@eslint/js": "^9.8.0",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "workspace:^",
"@types/byte-size": "^8.1.0", "@types/byte-size": "^8.1.0",
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",

6874
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,8 +21,8 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0", "@eslint/js": "^9.8.0",
"@immich/cli": "file:../cli", "@immich/cli": "workspace:^",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "workspace:^",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.15.32", "@types/node": "^22.15.32",

View File

@@ -146,7 +146,6 @@ dart_code_metrics:
# - no-empty-block # - no-empty-block
# - no-equal-then-else # - no-equal-then-else
# - prefer-correct-test-file-name # - prefer-correct-test-file-name
- prefer-const-border-radius
# - prefer-match-file-name # - prefer-match-file-name
# - prefer-return-await # - prefer-return-await
# - avoid-self-assignment # - avoid-self-assignment
@@ -291,8 +290,7 @@ dart_code_metrics:
# Style # Style
# - prefer-trailing-comma # - prefer-trailing-comma
# - unnecessary-trailing-comma # - unnecessary-trailing-comma
- prefer-declaring-const-constructor # - prefer-declaring-const-constructor
# - prefer-single-widget-per-file # - prefer-single-widget-per-file
- prefer-switch-expression
# - prefer-prefixed-global-constants # - prefer-prefixed-global-constants
# - prefer-correct-callback-field-name # - prefer-correct-callback-field-name

View File

@@ -7,7 +7,7 @@ import 'general_helper.dart';
class ImmichTestLoginHelper { class ImmichTestLoginHelper {
final WidgetTester tester; final WidgetTester tester;
const ImmichTestLoginHelper(this.tester); ImmichTestLoginHelper(this.tester);
Future<void> waitForLoginScreen() async { Future<void> waitForLoginScreen() async {
await pumpUntilFound(tester, find.text("Login")); await pumpUntilFound(tester, find.text("Login"));
@@ -60,11 +60,11 @@ class ImmichTestLoginHelper {
await tester.tap(button); await tester.tap(button);
} }
Future<void> assertLoginSuccess() async { Future<void> assertLoginSuccess({int timeoutSeconds = 15}) async {
await pumpUntilFound(tester, find.text("home_page_building_timeline".tr())); await pumpUntilFound(tester, find.text("home_page_building_timeline".tr()));
} }
Future<void> assertLoginFailed() async { Future<void> assertLoginFailed({int timeoutSeconds = 15}) async {
await pumpUntilFound(tester, find.text("login_form_failed_login".tr())); await pumpUntilFound(tester, find.text("login_form_failed_login".tr()));
} }
} }

View File

@@ -4,8 +4,6 @@ sealed class ImmichErrors {
} }
class NoResponseDtoError extends ImmichErrors implements Exception { class NoResponseDtoError extends ImmichErrors implements Exception {
const NoResponseDtoError();
@override @override
String toString() => "Response Dto is null"; String toString() => "Response Dto is null";
} }

View File

@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
class Person { class Person {
const Person({ Person({
required this.id, required this.id,
this.birthDate, this.birthDate,
required this.isHidden, required this.isHidden,

View File

@@ -554,12 +554,18 @@ class Asset {
}"""; }""";
} }
static getVisibility(AssetVisibility visibility) => switch (visibility) { static getVisibility(AssetVisibility visibility) {
AssetVisibility.archive => AssetVisibilityEnum.archive, switch (visibility) {
AssetVisibility.hidden => AssetVisibilityEnum.hidden, case AssetVisibility.timeline:
AssetVisibility.locked => AssetVisibilityEnum.locked, return AssetVisibilityEnum.timeline;
AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline, case AssetVisibility.archive:
}; return AssetVisibilityEnum.archive;
case AssetVisibility.hidden:
return AssetVisibilityEnum.hidden;
case AssetVisibility.locked:
return AssetVisibilityEnum.locked;
}
}
} }
enum AssetType { enum AssetType {

View File

@@ -11,7 +11,7 @@ class SSLClientCertStoreVal {
final Uint8List data; final Uint8List data;
final String? password; final String? password;
const SSLClientCertStoreVal(this.data, this.password); SSLClientCertStoreVal(this.data, this.password);
void save() { void save() {
final b64Str = base64Encode(data); final b64Str = base64Encode(data);

View File

@@ -5,7 +5,7 @@ class ApiRepository {
Future<T> checkNull<T>(Future<T?> future) async { Future<T> checkNull<T>(Future<T?> future) async {
final response = await future; final response = await future;
if (response == null) throw const NoResponseDtoError(); if (response == null) throw NoResponseDtoError();
return response; return response;
} }
} }

View File

@@ -5,7 +5,7 @@ class AlbumViewerPageState {
final String editTitleText; final String editTitleText;
final String editDescriptionText; final String editDescriptionText;
const AlbumViewerPageState({ AlbumViewerPageState({
required this.isEditAlbum, required this.isEditAlbum,
required this.editTitleText, required this.editTitleText,
required this.editDescriptionText, required this.editDescriptionText,

View File

@@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class AssetSelectionPageResult { class AssetSelectionPageResult {
final Set<Asset> selectedAssets; final Set<Asset> selectedAssets;
const AssetSelectionPageResult({ AssetSelectionPageResult({
required this.selectedAssets, required this.selectedAssets,
}); });
@override @override

View File

@@ -7,7 +7,7 @@ class AuthState {
final bool isAdmin; final bool isAdmin;
final String profileImagePath; final String profileImagePath;
const AuthState({ AuthState({
required this.deviceId, required this.deviceId,
required this.userId, required this.userId,
required this.userEmail, required this.userEmail,

View File

@@ -5,7 +5,7 @@ class AuxilaryEndpoint {
final String url; final String url;
final AuxCheckStatus status; final AuxCheckStatus status;
const AuxilaryEndpoint({ AuxilaryEndpoint({
required this.url, required this.url,
required this.status, required this.status,
}); });
@@ -55,7 +55,7 @@ class AuxilaryEndpoint {
class AuxCheckStatus { class AuxCheckStatus {
final String name; final String name;
const AuxCheckStatus({ AuxCheckStatus({
required this.name, required this.name,
}); });
const AuxCheckStatus._(this.name); const AuxCheckStatus._(this.name);

View File

@@ -13,7 +13,7 @@ class LoginResponse {
final String userId; final String userId;
const LoginResponse({ LoginResponse({
required this.accessToken, required this.accessToken,
required this.isAdmin, required this.isAdmin,
required this.name, required this.name,

View File

@@ -4,7 +4,7 @@ class AvailableAlbum {
final Album album; final Album album;
final int assetCount; final int assetCount;
final DateTime? lastBackup; final DateTime? lastBackup;
const AvailableAlbum({ AvailableAlbum({
required this.album, required this.album,
required this.assetCount, required this.assetCount,
this.lastBackup, this.lastBackup,

View File

@@ -9,7 +9,7 @@ class CurrentUploadAsset {
final int? fileSize; final int? fileSize;
final bool? iCloudAsset; final bool? iCloudAsset;
const CurrentUploadAsset({ CurrentUploadAsset({
required this.id, required this.id,
required this.fileCreatedAt, required this.fileCreatedAt,
required this.fileName, required this.fileName,

View File

@@ -5,7 +5,7 @@ class SuccessUploadAsset {
final String remoteAssetId; final String remoteAssetId;
final bool isDuplicate; final bool isDuplicate;
const SuccessUploadAsset({ SuccessUploadAsset({
required this.candidate, required this.candidate,
required this.remoteAssetId, required this.remoteAssetId,
required this.isDuplicate, required this.isDuplicate,

View File

@@ -10,7 +10,7 @@ class DownloadInfo {
// enum // enum
final TaskStatus status; final TaskStatus status;
const DownloadInfo({ DownloadInfo({
required this.fileName, required this.fileName,
required this.progress, required this.progress,
required this.status, required this.status,
@@ -71,7 +71,7 @@ class DownloadState {
final TaskStatus downloadStatus; final TaskStatus downloadStatus;
final Map<String, DownloadInfo> taskProgress; final Map<String, DownloadInfo> taskProgress;
final bool showProgress; final bool showProgress;
const DownloadState({ DownloadState({
required this.downloadStatus, required this.downloadStatus,
required this.taskProgress, required this.taskProgress,
required this.showProgress, required this.showProgress,

View File

@@ -3,7 +3,7 @@ import 'package:immich_mobile/models/folder/root_folder.model.dart';
class RecursiveFolder extends RootFolder { class RecursiveFolder extends RootFolder {
final String name; final String name;
const RecursiveFolder({ RecursiveFolder({
required this.name, required this.name,
required super.path, required super.path,
required super.subfolders, required super.subfolders,

View File

@@ -4,7 +4,7 @@ class RootFolder {
final List<RecursiveFolder> subfolders; final List<RecursiveFolder> subfolders;
final String path; final String path;
const RootFolder({ RootFolder({
required this.subfolders, required this.subfolders,
required this.path, required this.path,
}); });

View File

@@ -8,6 +8,4 @@ class MapAssetsInBoundsUpdated extends MapEvent {
const MapAssetsInBoundsUpdated(this.assetRemoteIds); const MapAssetsInBoundsUpdated(this.assetRemoteIds);
} }
class MapCloseBottomSheet extends MapEvent { class MapCloseBottomSheet extends MapEvent {}
const MapCloseBottomSheet();
}

View File

@@ -4,7 +4,7 @@ import 'package:openapi/api.dart';
class MapMarker { class MapMarker {
final LatLng latLng; final LatLng latLng;
final String assetRemoteId; final String assetRemoteId;
const MapMarker({ MapMarker({
required this.latLng, required this.latLng,
required this.assetRemoteId, required this.assetRemoteId,
}); });

View File

@@ -11,7 +11,7 @@ class MapState {
final AsyncValue<String> lightStyleFetched; final AsyncValue<String> lightStyleFetched;
final AsyncValue<String> darkStyleFetched; final AsyncValue<String> darkStyleFetched;
const MapState({ MapState({
this.themeMode = ThemeMode.system, this.themeMode = ThemeMode.system,
this.showFavoriteOnly = false, this.showFavoriteOnly = false,
this.includeArchived = false, this.includeArchived = false,

View File

@@ -7,7 +7,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class Memory { class Memory {
final String title; final String title;
final List<Asset> assets; final List<Asset> assets;
const Memory({ Memory({
required this.title, required this.title,
required this.assets, required this.assets,
}); });

View File

@@ -14,7 +14,7 @@ class SearchCuratedContent {
/// The id to lookup the asset from the server /// The id to lookup the asset from the server
final String id; final String id;
const SearchCuratedContent({ SearchCuratedContent({
required this.label, required this.label,
required this.id, required this.id,
this.subtitle, this.subtitle,

View File

@@ -6,7 +6,7 @@ class SearchResult {
final List<Asset> assets; final List<Asset> assets;
final int? nextPage; final int? nextPage;
const SearchResult({ SearchResult({
required this.assets, required this.assets,
this.nextPage, this.nextPage,
}); });

View File

@@ -8,7 +8,7 @@ class SearchResultPageState {
final bool isSmart; final bool isSmart;
final List<Asset> searchResult; final List<Asset> searchResult;
const SearchResultPageState({ SearchResultPageState({
required this.isLoading, required this.isLoading,
required this.isSuccess, required this.isSuccess,
required this.isError, required this.isError,

View File

@@ -13,7 +13,7 @@ class ServerInfo {
final bool isNewReleaseAvailable; final bool isNewReleaseAvailable;
final String versionMismatchErrorMessage; final String versionMismatchErrorMessage;
const ServerInfo({ ServerInfo({
required this.serverVersion, required this.serverVersion,
required this.latestVersion, required this.latestVersion,
required this.serverFeatures, required this.serverFeatures,

View File

@@ -105,9 +105,7 @@ class AlbumsPage extends HookConsumerWidget {
color: context.colorScheme.onSurface.withAlpha(0), color: context.colorScheme.onSurface.withAlpha(0),
width: 0, width: 0,
), ),
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(24),
Radius.circular(24),
),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
context.colorScheme.primary.withValues(alpha: 0.075), context.colorScheme.primary.withValues(alpha: 0.075),
@@ -303,9 +301,7 @@ class QuickFilterButton extends StatelessWidget {
), ),
shape: WidgetStateProperty.all( shape: WidgetStateProperty.all(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
side: BorderSide( side: BorderSide(
color: context.colorScheme.onSurface.withAlpha(25), color: context.colorScheme.onSurface.withAlpha(25),
width: 1, width: 1,
@@ -338,10 +334,8 @@ class SortButton extends ConsumerWidget {
style: MenuStyle( style: MenuStyle(
elevation: const WidgetStatePropertyAll(1), elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all( shape: WidgetStateProperty.all(
const RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(24),
Radius.circular(24),
),
), ),
), ),
padding: const WidgetStatePropertyAll( padding: const WidgetStatePropertyAll(
@@ -390,10 +384,8 @@ class SortButton extends ConsumerWidget {
: Colors.transparent, : Colors.transparent,
), ),
shape: WidgetStateProperty.all( shape: WidgetStateProperty.all(
const RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(24),
Radius.circular(24),
),
), ),
), ),
), ),

View File

@@ -246,10 +246,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
shape: const RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(10),
Radius.circular(10),
),
), ),
elevation: 5, elevation: 5,
title: Text( title: Text(

View File

@@ -147,9 +147,7 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Card( child: Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
side: BorderSide( side: BorderSide(
color: context.colorScheme.outlineVariant, color: context.colorScheme.outlineVariant,
width: 1, width: 1,

View File

@@ -42,11 +42,9 @@ class FailedBackupStatusPage extends HookConsumerWidget {
vertical: 4, vertical: 4,
), ),
child: Card( child: Card(
shape: const RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(15), // if you need this
Radius.circular(15), // if you need this side: const BorderSide(
),
side: BorderSide(
color: Colors.black12, color: Colors.black12,
width: 1, width: 1,
), ),

View File

@@ -60,9 +60,7 @@ class AppLogDetailPage extends HookConsumerWidget {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerHigh, color: context.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(15.0),
Radius.circular(15.0),
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@@ -101,9 +99,7 @@ class AppLogDetailPage extends HookConsumerWidget {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerHigh, color: context.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(15.0),
Radius.circular(15.0),
),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),

View File

@@ -120,10 +120,8 @@ class CreateAlbumPage extends HookConsumerWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: padding:
const EdgeInsets.symmetric(vertical: 24, horizontal: 16), const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
shape: const RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(10),
Radius.circular(10),
),
), ),
backgroundColor: context.colorScheme.surfaceContainerHigh, backgroundColor: context.colorScheme.surfaceContainerHigh,
), ),

View File

@@ -90,10 +90,8 @@ class DownloadTaskTile extends StatelessWidget {
width: context.width - 32, width: context.width - 32,
child: Card( child: Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(16),
Radius.circular(16),
),
), ),
child: ListTile( child: ListTile(
minVerticalPadding: 18, minVerticalPadding: 18,

View File

@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/advanced_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
@@ -12,6 +11,7 @@ import 'package:immich_mobile/widgets/settings/language_settings.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart';
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
import 'package:immich_mobile/routing/router.dart';
enum SettingSection { enum SettingSection {
advanced( advanced(
@@ -85,13 +85,12 @@ class SettingsPage extends StatelessWidget {
centerTitle: false, centerTitle: false,
title: const Text('settings').tr(), title: const Text('settings').tr(),
), ),
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), body: context.isMobile ? _MobileLayout() : _TabletLayout(),
); );
} }
} }
class _MobileLayout extends StatelessWidget { class _MobileLayout extends StatelessWidget {
const _MobileLayout();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return ListView(
@@ -148,7 +147,6 @@ class _MobileLayout extends StatelessWidget {
} }
class _TabletLayout extends HookWidget { class _TabletLayout extends HookWidget {
const _TabletLayout();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedSection = final selectedSection =

View File

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

View File

@@ -124,9 +124,7 @@ class EditImagePage extends ConsumerWidget {
), ),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(7),
Radius.circular(7),
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.2), color: Colors.black.withValues(alpha: 0.2),
@@ -137,9 +135,7 @@ class EditImagePage extends ConsumerWidget {
], ],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(7),
Radius.circular(7),
),
child: Image( child: Image(
image: image.image, image: image.image,
fit: BoxFit.contain, fit: BoxFit.contain,
@@ -153,9 +149,7 @@ class EditImagePage extends ConsumerWidget {
margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.scaffoldBackgroundColor, color: context.scaffoldBackgroundColor,
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(30),
Radius.circular(30),
),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,

View File

@@ -162,17 +162,13 @@ class _FilterButton extends StatelessWidget {
width: 80, width: 80,
height: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(10),
Radius.circular(10),
),
border: isSelected border: isSelected
? Border.all(color: context.primaryColor, width: 3) ? Border.all(color: context.primaryColor, width: 3)
: null, : null,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(10),
Radius.circular(10),
),
child: ColorFiltered( child: ColorFiltered(
colorFilter: filter, colorFilter: filter,
child: FittedBox( child: FittedBox(

View File

@@ -105,9 +105,7 @@ class QuickAccessButtons extends ConsumerWidget {
color: context.colorScheme.onSurface.withAlpha(10), color: context.colorScheme.onSurface.withAlpha(10),
width: 1, width: 1,
), ),
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(10),
@@ -242,9 +240,7 @@ class PeopleCollectionCard extends ConsumerWidget {
height: size, height: size,
width: size, width: size,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(30),

View File

@@ -80,9 +80,7 @@ class PartnerDetailPage extends HookConsumerWidget {
color: context.colorScheme.onSurface.withAlpha(10), color: context.colorScheme.onSurface.withAlpha(10),
width: 1, width: 1,
), ),
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
context.colorScheme.primary.withAlpha(10), context.colorScheme.primary.withAlpha(10),

View File

@@ -143,9 +143,7 @@ class PlaceTile extends StatelessWidget {
), ),
), ),
leading: ClipRRect( leading: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
child: CachedNetworkImage( child: CachedNetworkImage(
width: 80, width: 80,
height: 80, height: 80,

View File

@@ -156,7 +156,7 @@ class MapPage extends HookConsumerWidget {
} else { } else {
// If no asset was previously selected and no new asset is available, close the bottom sheet // If no asset was previously selected and no new asset is available, close the bottom sheet
if (selectedMarker.value == null) { if (selectedMarker.value == null) {
bottomSheetStreamController.add(const MapCloseBottomSheet()); bottomSheetStreamController.add(MapCloseBottomSheet());
} }
selectedMarker.value = null; selectedMarker.value = null;
} }

View File

@@ -511,11 +511,16 @@ class SearchPage extends HookConsumerWidget {
search(); search();
} }
IconData getSearchPrefixIcon() => switch (textSearchType.value) { IconData getSearchPrefixIcon() {
TextSearchType.context => Icons.image_search_rounded, switch (textSearchType.value) {
TextSearchType.filename => Icons.abc_rounded, case TextSearchType.context:
TextSearchType.description => Icons.text_snippet_outlined, return Icons.image_search_rounded;
}; case TextSearchType.filename:
return Icons.abc_rounded;
case TextSearchType.description:
return Icons.text_snippet_outlined;
}
}
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
@@ -528,10 +533,8 @@ class SearchPage extends HookConsumerWidget {
style: MenuStyle( style: MenuStyle(
elevation: const WidgetStatePropertyAll(1), elevation: const WidgetStatePropertyAll(1),
shape: WidgetStateProperty.all( shape: WidgetStateProperty.all(
const RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.circular(24),
Radius.circular(24),
),
), ),
), ),
padding: const WidgetStatePropertyAll( padding: const WidgetStatePropertyAll(
@@ -628,9 +631,7 @@ class SearchPage extends HookConsumerWidget {
color: context.colorScheme.onSurface.withAlpha(0), color: context.colorScheme.onSurface.withAlpha(0),
width: 0, width: 0,
), ),
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(24),
Radius.circular(24),
),
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
context.colorScheme.primary.withValues(alpha: 0.075), context.colorScheme.primary.withValues(alpha: 0.075),
@@ -822,9 +823,7 @@ class QuickLinkList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all( borderRadius: BorderRadius.circular(20),
Radius.circular(20),
),
border: Border.all( border: Border.all(
color: context.colorScheme.outline.withAlpha(10), color: context.colorScheme.outline.withAlpha(10),
width: 1, width: 1,

View File

@@ -91,7 +91,7 @@ final _features = [
_Feature( _Feature(
name: 'Main Timeline', name: 'Main Timeline',
icon: Icons.timeline_rounded, icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), onTap: (ctx, _) => ctx.pushRoute(const MainTimelineRoute()),
), ),
]; ];

View File

@@ -15,7 +15,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
final double width; final double width;
final CacheManager? cacheManager; final CacheManager? cacheManager;
const RemoteThumbProvider({ RemoteThumbProvider({
required this.assetId, required this.assetId,
this.height = kTimelineFixedTileExtent, this.height = kTimelineFixedTileExtent,
this.width = kTimelineFixedTileExtent, this.width = kTimelineFixedTileExtent,

View File

@@ -8,7 +8,7 @@ import 'package:thumbhash/thumbhash.dart';
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> { class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
final String thumbHash; final String thumbHash;
const ThumbHashProvider({ ThumbHashProvider({
required this.thumbHash, required this.thumbHash,
}); });

View File

@@ -63,8 +63,7 @@ class FixedSegment extends Segment {
} }
void _handleOnTap(WidgetRef ref, BaseAsset asset) { void _handleOnTap(WidgetRef ref, BaseAsset asset) {
final multiSelectState = ref.read(multiSelectProvider); if (!ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
if (!multiSelectState.isEnabled) {
return; return;
} }
@@ -72,8 +71,7 @@ class FixedSegment extends Segment {
} }
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) { void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
final multiSelectState = ref.read(multiSelectProvider); if (ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
if (multiSelectState.isEnabled) {
return; return;
} }
@@ -100,63 +98,55 @@ class FixedSegment extends Segment {
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets); return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
} }
Widget _buildRow(int assetIndex, int count) => RepaintBoundary( Widget _buildRow(int assetIndex, int count) => Consumer(
child: Consumer( builder: (ctx, ref, _) {
builder: (ctx, ref, _) { final isScrubbing =
final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); final timelineService = ref.read(timelineServiceProvider);
final timelineService = ref.read(timelineServiceProvider);
// Create stable callback references to prevent unnecessary rebuilds // Timeline is being scrubbed, show placeholders
onTap(BaseAsset asset) => _handleOnTap(ref, asset); if (isScrubbing) {
onLongPress(BaseAsset asset) => _handleOnLongPress(ref, asset); return SegmentBuilder.buildPlaceholder(
ctx,
// Timeline is being scrubbed, show placeholders count,
if (isScrubbing) { size: Size.square(tileHeight),
return SegmentBuilder.buildPlaceholder( spacing: spacing,
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,
);
},
); );
}, }
),
// Bucket is already loaded, show the assets
if (timelineService.hasRange(assetIndex, count)) {
final assets = timelineService.getAssets(assetIndex, count);
return _buildAssetRow(
ctx,
assets,
onTap: (asset) => _handleOnTap(ref, asset),
onLongPress: (asset) => _handleOnLongPress(ref, asset),
);
}
// 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,
onTap: (asset) => _handleOnTap(ref, asset),
onLongPress: (asset) => _handleOnLongPress(ref, asset),
);
},
);
},
); );
Widget _buildAssetRow( Widget _buildAssetRow(
@@ -164,7 +154,6 @@ class FixedSegment extends Segment {
List<BaseAsset> assets, { List<BaseAsset> assets, {
required void Function(BaseAsset) onTap, required void Function(BaseAsset) onTap,
required void Function(BaseAsset) onLongPress, required void Function(BaseAsset) onLongPress,
required int baseAssetIndex,
}) => }) =>
FixedTimelineRow( FixedTimelineRow(
dimension: tileHeight, dimension: tileHeight,
@@ -172,59 +161,13 @@ class FixedSegment extends Segment {
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
children: List.generate( children: List.generate(
assets.length, assets.length,
(i) => _AssetTileWidget( (i) => RepaintBoundary(
key: ValueKey(_generateUniqueKey(assets[i], baseAssetIndex + i)), child: GestureDetector(
asset: assets[i], onTap: () => onTap(assets[i]),
onTap: onTap, onLongPress: () => onLongPress(assets[i]),
onLongPress: onLongPress, child: ThumbnailTile(assets[i]),
),
), ),
), ),
); );
/// 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),
),
);
}
} }

View File

@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_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/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -81,15 +80,12 @@ class TimelineHeader extends ConsumerWidget {
if (header != HeaderType.monthAndDay) if (header != HeaderType.monthAndDay)
_BulkSelectIconButton( _BulkSelectIconButton(
isAllSelected: isAllSelected, isAllSelected: isAllSelected,
onPressed: () { onPressed: () => ref
ref .read(multiSelectProvider.notifier)
.read(multiSelectProvider.notifier) .toggleBucketSelection(
.toggleBucketSelection( assetOffset,
assetOffset, bucket.assetCount,
bucket.assetCount, ),
);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
), ),
], ],
), ),
@@ -105,15 +101,9 @@ class TimelineHeader extends ConsumerWidget {
const Spacer(), const Spacer(),
_BulkSelectIconButton( _BulkSelectIconButton(
isAllSelected: isAllSelected, isAllSelected: isAllSelected,
onPressed: () { onPressed: () => ref
ref .read(multiSelectProvider.notifier)
.read(multiSelectProvider.notifier) .toggleBucketSelection(assetOffset, bucket.assetCount),
.toggleBucketSelection(
assetOffset,
bucket.assetCount,
);
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
},
), ),
], ],
), ),

View File

@@ -44,16 +44,12 @@ List<_Segment> _buildSegments({
required List<Segment> layoutSegments, required List<Segment> layoutSegments,
required double timelineHeight, required double timelineHeight,
}) { }) {
const double offsetThreshold = 20.0;
final segments = <_Segment>[]; final segments = <_Segment>[];
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) { if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
return []; return [];
} }
final formatter = DateFormat.yMMM(); final formatter = DateFormat.yMMM();
DateTime? lastDate;
double lastOffset = -offsetThreshold;
for (final layoutSegment in layoutSegments) { for (final layoutSegment in layoutSegments) {
final scrollPercentage = final scrollPercentage =
layoutSegment.startOffset / layoutSegments.last.endOffset; layoutSegment.startOffset / layoutSegments.last.endOffset;
@@ -62,21 +58,13 @@ List<_Segment> _buildSegments({
final date = (layoutSegment.bucket as TimeBucket).date; final date = (layoutSegment.bucket as TimeBucket).date;
final label = formatter.format(date); final label = formatter.format(date);
final showSegment = lastOffset + offsetThreshold <= startOffset &&
(lastDate == null || date.year != lastDate.year);
segments.add( segments.add(
_Segment( _Segment(
date: date, date: date,
startOffset: startOffset, startOffset: startOffset,
scrollLabel: label, scrollLabel: label,
showSegment: showSegment,
), ),
); );
lastDate = date;
if (showSegment) {
lastOffset = startOffset;
}
} }
return segments; return segments;
@@ -97,15 +85,12 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
double get _scrubberHeight => double get _scrubberHeight =>
widget.timelineHeight - widget.topPadding - widget.bottomPadding; widget.timelineHeight - widget.topPadding - widget.bottomPadding;
late ScrollController _scrollController; late final ScrollController _scrollController;
double get _currentOffset { double get _currentOffset =>
if (_scrollController.hasClients != true) return 0.0; _scrollController.offset *
_scrubberHeight /
return _scrollController.offset * _scrollController.position.maxScrollExtent;
_scrubberHeight /
_scrollController.position.maxScrollExtent;
}
@override @override
void initState() { void initState() {
@@ -209,102 +194,28 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
_thumbAnimationController.forward(); _thumbAnimationController.forward();
} }
final dragPosition = _calculateDragPosition(details); final newOffset =
final nearestMonthSegment = _findNearestMonthSegment(dragPosition); details.globalPosition.dy - widget.topPadding - widget.bottomPadding;
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(() { setState(() {
_thumbTopOffset = segment.startOffset; _thumbTopOffset = newOffset.clamp(0, _scrubberHeight);
final scrollPercentage = _thumbTopOffset / _scrubberHeight;
final layoutSegmentIndex = _findLayoutSegmentIndex(segment); final maxScrollExtent = _scrollController.position.maxScrollExtent;
_scrollController.jumpTo(maxScrollExtent * scrollPercentage);
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) { void _onDragEnd(WidgetRef ref) {
ref.read(timelineStateProvider.notifier).setScrubbing(false); ref.read(timelineStateProvider.notifier).setScrubbing(false);
_labelAnimationController.reverse(); _labelAnimationController.reverse();
_isDragging = false; _isDragging = false;
_resetThumbTimer(); _resetThumbTimer();
} }
@override @override
Widget build(BuildContext ctx) { Widget build(BuildContext ctx) {
Text? label; Text? label;
if (_scrollController.hasClients == true) { if (_scrollController.hasClients) {
// Cache to avoid multiple calls to [_currentOffset] // Cache to avoid multiple calls to [_currentOffset]
final scrollOffset = _currentOffset; final scrollOffset = _currentOffset;
final labelText = _segments final labelText = _segments
@@ -329,31 +240,20 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
child: Stack( child: Stack(
children: [ children: [
RepaintBoundary(child: widget.child), 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( PositionedDirectional(
top: _thumbTopOffset + widget.topPadding, top: _thumbTopOffset + widget.topPadding,
end: 0, end: 0,
child: RepaintBoundary( child: Consumer(
child: Consumer( builder: (_, ref, child) => GestureDetector(
builder: (_, ref, child) => GestureDetector( onVerticalDragStart: (_) => _onDragStart(ref),
onVerticalDragStart: (_) => _onDragStart(ref), onVerticalDragUpdate: _onDragUpdate,
onVerticalDragUpdate: _onDragUpdate, onVerticalDragEnd: (_) => _onDragEnd(ref),
onVerticalDragEnd: (_) => _onDragEnd(ref), child: child,
child: child, ),
), child: _Scrubber(
child: _Scrubber( thumbAnimation: _thumbAnimation,
thumbAnimation: _thumbAnimation, labelAnimation: _labelAnimation,
labelAnimation: _labelAnimation, label: label,
label: label,
),
), ),
), ),
), ),
@@ -363,72 +263,6 @@ 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 { class _ScrollLabel extends StatelessWidget {
final Text label; final Text label;
final Color backgroundColor; final Color backgroundColor;
@@ -595,26 +429,22 @@ class _Segment {
final DateTime date; final DateTime date;
final double startOffset; final double startOffset;
final String scrollLabel; final String scrollLabel;
final bool showSegment;
const _Segment({ const _Segment({
required this.date, required this.date,
required this.startOffset, required this.startOffset,
required this.scrollLabel, required this.scrollLabel,
this.showSegment = false,
}); });
_Segment copyWith({ _Segment copyWith({
DateTime? date, DateTime? date,
double? startOffset, double? startOffset,
String? scrollLabel, String? scrollLabel,
bool? showSegment,
}) { }) {
return _Segment( return _Segment(
date: date ?? this.date, date: date ?? this.date,
startOffset: startOffset ?? this.startOffset, startOffset: startOffset ?? this.startOffset,
scrollLabel: scrollLabel ?? this.scrollLabel, scrollLabel: scrollLabel ?? this.scrollLabel,
showSegment: showSegment ?? this.showSegment,
); );
} }

View File

@@ -15,12 +15,18 @@ abstract class SegmentBuilder {
this.groupBy = GroupAssetsBy.day, this.groupBy = GroupAssetsBy.day,
}); });
static double headerExtent(HeaderType header) => switch (header) { static double headerExtent(HeaderType header) {
HeaderType.month => kTimelineHeaderExtent, switch (header) {
HeaderType.day => kTimelineHeaderExtent * 0.90, case HeaderType.month:
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6, return kTimelineHeaderExtent;
HeaderType.none => 0.0, case HeaderType.day:
}; return kTimelineHeaderExtent * 0.90;
case HeaderType.monthAndDay:
return kTimelineHeaderExtent * 1.6;
case HeaderType.none:
return 0.0;
}
}
static Widget buildPlaceholder( static Widget buildPlaceholder(
BuildContext context, BuildContext context,

View File

@@ -13,8 +13,6 @@ import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.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/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.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 { class Timeline extends StatelessWidget {
const Timeline({super.key}); const Timeline({super.key});
@@ -65,68 +63,38 @@ class _SliverTimelineState extends State<_SliverTimeline> {
final asyncSegments = ref.watch(timelineSegmentProvider); final asyncSegments = ref.watch(timelineSegmentProvider);
final maxHeight = final maxHeight =
ref.watch(timelineArgsProvider.select((args) => args.maxHeight)); ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
final isMultiSelectEnabled =
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
return asyncSegments.widgetWhen( return asyncSegments.widgetWhen(
onData: (segments) { onData: (segments) {
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1; final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
final statusBarHeight = context.padding.top;
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
const scrubberBottomPadding = 100.0;
return PrimaryScrollController( return PrimaryScrollController(
controller: _scrollController, controller: _scrollController,
child: Stack( child: Scrubber(
children: [ layoutSegments: segments,
Scrubber( timelineHeight: maxHeight,
layoutSegments: segments, topPadding: context.padding.top + 10,
timelineHeight: maxHeight, bottomPadding: context.padding.bottom + 10,
topPadding: totalAppBarHeight + 10, child: CustomScrollView(
bottomPadding: primary: true,
context.padding.bottom + scrubberBottomPadding, cacheExtent: maxHeight * 2,
child: CustomScrollView( slivers: [
primary: true, _SliverSegmentedList(
cacheExtent: maxHeight * 2, segments: segments,
slivers: [ delegate: SliverChildBuilderDelegate(
SliverAnimatedOpacity( (ctx, index) {
duration: Durations.medium1, if (index >= childCount) return null;
opacity: isMultiSelectEnabled ? 0 : 1, final segment = segments.findByIndex(index);
sliver: const ImmichSliverAppBar( return segment?.builder(ctx, index) ??
floating: true, const SizedBox.shrink();
pinned: false, },
snap: false, childCount: childCount,
), addAutomaticKeepAlives: false,
), // We add repaint boundary around tiles, so skip the auto boundaries
_SliverSegmentedList( addRepaintBoundaries: false,
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(),
),
],
), ),
); );
}, },
@@ -395,27 +363,3 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
childManager.didFinishLayout(); 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,
),
),
);
}
}

View File

@@ -1,12 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/models/albums/album_viewer_page_state.model.dart';
import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> { class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref) AlbumViewerNotifier(this.ref)
: super( : super(
const AlbumViewerPageState( AlbumViewerPageState(
editTitleText: "", editTitleText: "",
isEditAlbum: false, isEditAlbum: false,
editDescriptionText: "", editDescriptionText: "",

View File

@@ -5,4 +5,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'app_settings.provider.g.dart'; part 'app_settings.provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
AppSettingsService appSettingsService(Ref _) => const AppSettingsService(); AppSettingsService appSettingsService(Ref _) => AppSettingsService();

View File

@@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
// ************************************************************************** // **************************************************************************
String _$appSettingsServiceHash() => String _$appSettingsServiceHash() =>
r'89cece3a19e06612f5639ae290120e854a0c5a31'; r'2aa16d76a8df869c39486325efc1d08b2d2c284c';
/// See also [appSettingsService]. /// See also [appSettingsService].
@ProviderFor(appSettingsService) @ProviderFor(appSettingsService)

View File

@@ -23,7 +23,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
this._shareService, this._shareService,
this._albumService, this._albumService,
) : super( ) : super(
const DownloadState( DownloadState(
downloadStatus: TaskStatus.complete, downloadStatus: TaskStatus.complete,
showProgress: false, showProgress: false,
taskProgress: <String, DownloadInfo>{}, taskProgress: <String, DownloadInfo>{},

View File

@@ -45,7 +45,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
this._secureStorageService, this._secureStorageService,
this._widgetService, this._widgetService,
) : super( ) : super(
const AuthState( AuthState(
deviceId: "", deviceId: "",
userId: "", userId: "",
userEmail: "", userEmail: "",
@@ -89,7 +89,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
} }
Future<void> _cleanUp() async { Future<void> _cleanUp() async {
state = const AuthState( state = AuthState(
deviceId: "", deviceId: "",
userId: "", userId: "",
userEmail: "", userEmail: "",

View File

@@ -7,7 +7,7 @@ class IOSBackgroundSettings {
final DateTime? timeOfLastFetch; final DateTime? timeOfLastFetch;
final DateTime? timeOfLastProcessing; final DateTime? timeOfLastProcessing;
const IOSBackgroundSettings({ IOSBackgroundSettings({
required this.appRefreshEnabled, required this.appRefreshEnabled,
required this.numberOfBackgroundTasksQueued, required this.numberOfBackgroundTasksQueued,
this.timeOfLastFetch, this.timeOfLastFetch,

View File

@@ -42,6 +42,6 @@ class ImageLoader {
} }
// If we get here, the image failed to load from the cache stream // If we get here, the image failed to load from the cache stream
throw const ImageLoadingException('Could not load image from stream'); throw ImageLoadingException('Could not load image from stream');
} }
} }

View File

@@ -1,5 +1,5 @@
/// An exception for the [ImageLoader] and the Immich image providers /// An exception for the [ImageLoader] and the Immich image providers
class ImageLoadingException implements Exception { class ImageLoadingException implements Exception {
final String message; final String message;
const ImageLoadingException(this.message); ImageLoadingException(this.message);
} }

View File

@@ -23,7 +23,7 @@ class ImmichRemoteImageProvider
/// The image cache manager /// The image cache manager
final CacheManager? cacheManager; final CacheManager? cacheManager;
const ImmichRemoteImageProvider({ ImmichRemoteImageProvider({
required this.assetId, required this.assetId,
this.cacheManager, this.cacheManager,
}); });

View File

@@ -24,7 +24,7 @@ class ImmichRemoteThumbnailProvider
/// The image cache manager /// The image cache manager
final CacheManager? cacheManager; final CacheManager? cacheManager;
const ImmichRemoteThumbnailProvider({ ImmichRemoteThumbnailProvider({
required this.assetId, required this.assetId,
this.height, this.height,
this.width, this.width,

View File

@@ -17,7 +17,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
final SearchService _searchService; final SearchService _searchService;
PaginatedSearchNotifier(this._searchService) PaginatedSearchNotifier(this._searchService)
: super(const SearchResult(assets: [], nextPage: 1)); : super(SearchResult(assets: [], nextPage: 1));
Future<bool> search(SearchFilter filter) async { Future<bool> search(SearchFilter filter) async {
if (state.nextPage == null) { if (state.nextPage == null) {
@@ -39,7 +39,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
} }
clear() { clear() {
state = const SearchResult(assets: [], nextPage: 1); state = SearchResult(assets: [], nextPage: 1);
} }
} }

View File

@@ -1,35 +1,36 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.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_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_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: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_features.model.dart';
import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
class ServerInfoNotifier extends StateNotifier<ServerInfo> { class ServerInfoNotifier extends StateNotifier<ServerInfo> {
ServerInfoNotifier(this._serverInfoService) ServerInfoNotifier(this._serverInfoService)
: super( : super(
const ServerInfo( ServerInfo(
serverVersion: ServerVersion( serverVersion: const ServerVersion(
major: 0, major: 0,
minor: 0, minor: 0,
patch: 0, patch: 0,
), ),
latestVersion: ServerVersion( latestVersion: const ServerVersion(
major: 0, major: 0,
minor: 0, minor: 0,
patch: 0, patch: 0,
), ),
serverFeatures: ServerFeatures( serverFeatures: const ServerFeatures(
map: true, map: true,
trash: true, trash: true,
oauthEnabled: false, oauthEnabled: false,
passwordLogin: true, passwordLogin: true,
), ),
serverConfig: ServerConfig( serverConfig: const ServerConfig(
trashDays: 30, trashDays: 30,
oauthButtonText: '', oauthButtonText: '',
externalDomain: '', externalDomain: '',
@@ -37,7 +38,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
'https://tiles.immich.cloud/v1/style/light.json', 'https://tiles.immich.cloud/v1/style/light.json',
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
), ),
serverDiskInfo: ServerDiskInfo( serverDiskInfo: const ServerDiskInfo(
diskAvailable: "0", diskAvailable: "0",
diskSize: "0", diskSize: "0",
diskUse: "0", diskUse: "0",

View File

@@ -14,7 +14,7 @@ final multiSelectProvider =
class MultiSelectState { class MultiSelectState {
final Set<BaseAsset> selectedAssets; final Set<BaseAsset> selectedAssets;
const MultiSelectState({ MultiSelectState({
required this.selectedAssets, required this.selectedAssets,
}); });
@@ -50,7 +50,7 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
MultiSelectState build() { MultiSelectState build() {
_timelineService = ref.read(timelineServiceProvider); _timelineService = ref.read(timelineServiceProvider);
return const MultiSelectState( return MultiSelectState(
selectedAssets: {}, selectedAssets: {},
); );
} }
@@ -83,12 +83,6 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
} }
} }
void clearSelection() {
state = state.copyWith(
selectedAssets: {},
);
}
/// Bucket bulk operations /// Bucket bulk operations
void selectBucket(int offset, int bucketCount) async { void selectBucket(int offset, int bucketCount) async {
final assets = await _timelineService.loadAssets(offset, bucketCount); final assets = await _timelineService.loadAssets(offset, bucketCount);

View File

@@ -17,7 +17,7 @@ class UploadProfileImageState {
// enum // enum
final UploadProfileStatus status; final UploadProfileStatus status;
final String profileImagePath; final String profileImagePath;
const UploadProfileImageState({ UploadProfileImageState({
required this.status, required this.status,
required this.profileImagePath, required this.profileImagePath,
}); });
@@ -74,7 +74,7 @@ class UploadProfileImageNotifier
extends StateNotifier<UploadProfileImageState> { extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier(this._userService) UploadProfileImageNotifier(this._userService)
: super( : super(
const UploadProfileImageState( UploadProfileImageState(
profileImagePath: '', profileImagePath: '',
status: UploadProfileStatus.idle, status: UploadProfileStatus.idle,
), ),

View File

@@ -56,7 +56,7 @@ class WebsocketState {
final bool isConnected; final bool isConnected;
final List<PendingChange> pendingChanges; final List<PendingChange> pendingChanges;
const WebsocketState({ WebsocketState({
this.socket, this.socket,
required this.isConnected, required this.isConnected,
required this.pendingChanges, required this.pendingChanges,
@@ -94,11 +94,7 @@ class WebsocketState {
class WebsocketNotifier extends StateNotifier<WebsocketState> { class WebsocketNotifier extends StateNotifier<WebsocketState> {
WebsocketNotifier(this._ref) WebsocketNotifier(this._ref)
: super( : super(
const WebsocketState( WebsocketState(socket: null, isConnected: false, pendingChanges: []),
socket: null,
isConnected: false,
pendingChanges: [],
),
); );
final _log = Logger('WebsocketNotifier'); final _log = Logger('WebsocketNotifier');

View File

@@ -18,7 +18,7 @@ final albumRepositoryProvider =
Provider((ref) => AlbumRepository(ref.watch(dbProvider))); Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
class AlbumRepository extends DatabaseRepository { class AlbumRepository extends DatabaseRepository {
const AlbumRepository(super.db); AlbumRepository(super.db);
Future<int> count({bool? local}) { Future<int> count({bool? local}) {
final baseQuery = db.albums.where(); final baseQuery = db.albums.where();

View File

@@ -3,7 +3,7 @@ import 'package:immich_mobile/constants/errors.dart';
abstract class ApiRepository { abstract class ApiRepository {
Future<T> checkNull<T>(Future<T?> future) async { Future<T> checkNull<T>(Future<T?> future) async {
final response = await future; final response = await future;
if (response == null) throw const NoResponseDtoError(); if (response == null) throw NoResponseDtoError();
return response; return response;
} }
} }

View File

@@ -15,7 +15,7 @@ final assetRepositoryProvider =
Provider((ref) => AssetRepository(ref.watch(dbProvider))); Provider((ref) => AssetRepository(ref.watch(dbProvider)));
class AssetRepository extends DatabaseRepository { class AssetRepository extends DatabaseRepository {
const AssetRepository(super.db); AssetRepository(super.db);
Future<List<Asset>> getByAlbum( Future<List<Asset>> getByAlbum(
Album album, { Album album, {

View File

@@ -56,12 +56,18 @@ class AssetApiRepository extends ApiRepository {
); );
} }
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) { _mapVisibility(AssetVisibilityEnum visibility) {
AssetVisibilityEnum.timeline => AssetVisibility.timeline, switch (visibility) {
AssetVisibilityEnum.hidden => AssetVisibility.hidden, case AssetVisibilityEnum.timeline:
AssetVisibilityEnum.locked => AssetVisibility.locked, return AssetVisibility.timeline;
AssetVisibilityEnum.archive => AssetVisibility.archive, case AssetVisibilityEnum.hidden:
}; return AssetVisibility.hidden;
case AssetVisibilityEnum.locked:
return AssetVisibility.locked;
case AssetVisibilityEnum.archive:
return AssetVisibility.archive;
}
}
Future<String?> getAssetMIMEType(String assetId) async { Future<String?> getAssetMIMEType(String assetId) async {
final response = await checkNull(_api.getAssetInfo(assetId)); final response = await checkNull(_api.getAssetInfo(assetId));

View File

@@ -6,11 +6,9 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/hash.dart'; import 'package:immich_mobile/utils/hash.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType; import 'package:photo_manager/photo_manager.dart' hide AssetType;
final assetMediaRepositoryProvider = final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
Provider((ref) => const AssetMediaRepository());
class AssetMediaRepository { class AssetMediaRepository {
const AssetMediaRepository();
Future<List<String>> deleteAll(List<String> ids) => Future<List<String>> deleteAll(List<String> ids) =>
PhotoManager.editor.deleteWithIds(ids); PhotoManager.editor.deleteWithIds(ids);

View File

@@ -22,7 +22,7 @@ final authRepositoryProvider = Provider<AuthRepository>(
class AuthRepository extends DatabaseRepository { class AuthRepository extends DatabaseRepository {
final Drift _drift; final Drift _drift;
const AuthRepository(super.db, this._drift); AuthRepository(super.db, this._drift);
Future<void> clearLocalData() { Future<void> clearLocalData() {
return db.writeTxn(() { return db.writeTxn(() {

View File

@@ -10,7 +10,7 @@ final backupAlbumRepositoryProvider =
Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider)));
class BackupAlbumRepository extends DatabaseRepository { class BackupAlbumRepository extends DatabaseRepository {
const BackupAlbumRepository(super.db); BackupAlbumRepository(super.db);
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) { Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
final baseQuery = db.backupAlbums.where(); final baseQuery = db.backupAlbums.where();

View File

@@ -9,7 +9,7 @@ final biometricRepositoryProvider =
class BiometricRepository { class BiometricRepository {
final LocalAuthentication _localAuth; final LocalAuthentication _localAuth;
const BiometricRepository(this._localAuth); BiometricRepository(this._localAuth);
Future<BiometricStatus> getStatus() async { Future<BiometricStatus> getStatus() async {
final bool canAuthenticateWithBiometrics = final bool canAuthenticateWithBiometrics =

View File

@@ -7,7 +7,7 @@ const Symbol _zoneTxn = #zoneTxn;
abstract class DatabaseRepository implements IDatabaseRepository { abstract class DatabaseRepository implements IDatabaseRepository {
final Isar db; final Isar db;
const DatabaseRepository(this.db); DatabaseRepository(this.db);
bool get inTxn => Zone.current[_zoneTxn] != null; bool get inTxn => Zone.current[_zoneTxn] != null;

View File

@@ -8,7 +8,7 @@ final etagRepositoryProvider =
Provider((ref) => ETagRepository(ref.watch(dbProvider))); Provider((ref) => ETagRepository(ref.watch(dbProvider)));
class ETagRepository extends DatabaseRepository { class ETagRepository extends DatabaseRepository {
const ETagRepository(super.db); ETagRepository(super.db);
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll(); Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();

View File

@@ -6,11 +6,9 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType; import 'package:photo_manager/photo_manager.dart' hide AssetType;
final fileMediaRepositoryProvider = final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository());
Provider((ref) => const FileMediaRepository());
class FileMediaRepository { class FileMediaRepository {
const FileMediaRepository();
Future<Asset?> saveImage( Future<Asset?> saveImage(
Uint8List data, { Uint8List data, {
required String title, required String title,

View File

@@ -7,7 +7,7 @@ final localFilesManagerRepositoryProvider = Provider(
); );
class LocalFilesManagerRepository { class LocalFilesManagerRepository {
const LocalFilesManagerRepository(this._service); LocalFilesManagerRepository(this._service);
final LocalFilesManagerService _service; final LocalFilesManagerService _service;

View File

@@ -12,7 +12,7 @@ final networkRepositoryProvider = Provider((_) {
class NetworkRepository { class NetworkRepository {
final NetworkInfo _networkInfo; final NetworkInfo _networkInfo;
const NetworkRepository(this._networkInfo); NetworkRepository(this._networkInfo);
Future<String?> getWifiName() { Future<String?> getWifiName() {
if (Platform.isAndroid) { if (Platform.isAndroid) {

View File

@@ -11,7 +11,7 @@ final partnerRepositoryProvider = Provider(
); );
class PartnerRepository extends DatabaseRepository { class PartnerRepository extends DatabaseRepository {
const PartnerRepository(super.db); PartnerRepository(super.db);
Future<List<UserDto>> getSharedBy() async { Future<List<UserDto>> getSharedBy() async {
return (await db.users return (await db.users

View File

@@ -2,11 +2,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
final permissionRepositoryProvider = Provider((_) { final permissionRepositoryProvider = Provider((_) {
return const PermissionRepository(); return PermissionRepository();
}); });
class PermissionRepository implements IPermissionRepository { class PermissionRepository implements IPermissionRepository {
const PermissionRepository(); PermissionRepository();
@override @override
Future<bool> hasLocationWhenInUsePermission() { Future<bool> hasLocationWhenInUsePermission() {

View File

@@ -2,12 +2,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
final secureStorageRepositoryProvider = final secureStorageRepositoryProvider =
Provider((ref) => const SecureStorageRepository(FlutterSecureStorage())); Provider((ref) => SecureStorageRepository(const FlutterSecureStorage()));
class SecureStorageRepository { class SecureStorageRepository {
final FlutterSecureStorage _secureStorage; final FlutterSecureStorage _secureStorage;
const SecureStorageRepository(this._secureStorage); SecureStorageRepository(this._secureStorage);
Future<String?> read(String key) { Future<String?> read(String key) {
return _secureStorage.read(key: key); return _secureStorage.read(key: key);

View File

@@ -13,7 +13,7 @@ final timelineRepositoryProvider =
Provider((ref) => TimelineRepository(ref.watch(dbProvider))); Provider((ref) => TimelineRepository(ref.watch(dbProvider)));
class TimelineRepository extends DatabaseRepository { class TimelineRepository extends DatabaseRepository {
const TimelineRepository(super.db); TimelineRepository(super.db);
Future<List<String>> getTimelineUserIds(String id) { Future<List<String>> getTimelineUserIds(String id) {
return db.users return db.users

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