mirror of
https://github.com/immich-app/immich.git
synced 2025-12-17 10:07:51 -08:00
Compare commits
48 Commits
workflow-u
...
feat/disab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2248413280 | ||
|
|
f0b069adb9 | ||
|
|
276d02e12b | ||
|
|
ded9535434 | ||
|
|
997aec2441 | ||
|
|
cb2bd47816 | ||
|
|
f1c8377ca0 | ||
|
|
8416397589 | ||
|
|
dc29635b67 | ||
|
|
00290e1e71 | ||
|
|
3ef4c4f315 | ||
|
|
b10a8baf53 | ||
|
|
77926383db | ||
|
|
35eda735c8 | ||
|
|
8f7a71d1cf | ||
|
|
33cdea88aa | ||
|
|
4b345e02ff | ||
|
|
8cf900bafa | ||
|
|
59a3f0f455 | ||
|
|
c5d99711f7 | ||
|
|
4c0a41723f | ||
|
|
f73511a754 | ||
|
|
e637387082 | ||
|
|
baad38f0e6 | ||
|
|
161147af51 | ||
|
|
cbdf5011f9 | ||
|
|
f0f1d279c4 | ||
|
|
5821f2fe61 | ||
|
|
4cbce072be | ||
|
|
5e5bb7e87d | ||
|
|
b052893a1e | ||
|
|
15e58595fd | ||
|
|
6d499c782a | ||
|
|
7af99b8606 | ||
|
|
01e39277e0 | ||
|
|
06e79703da | ||
|
|
c360781565 | ||
|
|
287f6d5c94 | ||
|
|
fe9125a3d1 | ||
|
|
8b31936bb6 | ||
|
|
19958dfd83 | ||
|
|
1e1cf0d1fe | ||
|
|
879e0ea131 | ||
|
|
42136f9091 | ||
|
|
1109c32891 | ||
|
|
3c80049192 | ||
|
|
8f1669efbe | ||
|
|
146bf65d02 |
10
.github/workflows/build-mobile.yml
vendored
10
.github/workflows/build-mobile.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
- uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
@@ -222,6 +222,7 @@ jobs:
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
working-directory: ./mobile/ios
|
||||
|
||||
- name: Install CocoaPods dependencies
|
||||
@@ -229,13 +230,6 @@ jobs:
|
||||
run: |
|
||||
pod install
|
||||
|
||||
- name: Install Fastlane
|
||||
working-directory: ./mobile/ios
|
||||
run: |
|
||||
gem install bundler
|
||||
bundle config set --local path 'vendor/bundle'
|
||||
bundle install
|
||||
|
||||
- name: Create API Key
|
||||
env:
|
||||
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
|
||||
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
|
||||
# ℹ️ 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
|
||||
@@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
4
.github/workflows/fix-format.yml
vendored
4
.github/workflows/fix-format.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
2
.github/workflows/merge-translations.yml
vendored
2
.github/workflows/merge-translations.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
8
.github/workflows/prepare-release.yml
vendored
8
.github/workflows/prepare-release.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.IMMICH_VERSION }}
|
||||
|
||||
6
.github/workflows/release-pr.yml
vendored
6
.github/workflows/release-pr.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.result }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
2
.github/workflows/sdk.yml
vendored
2
.github/workflows/sdk.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
26
.github/workflows/test.yml
vendored
26
.github/workflows/test.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './cli/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -387,7 +387,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -481,7 +481,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -617,7 +617,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './.github/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -668,7 +668,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -730,7 +730,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/node": "^24.10.3",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -58,10 +58,6 @@ services:
|
||||
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
||||
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app
|
||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
ports:
|
||||
- 9230:9230
|
||||
- 9231:9231
|
||||
@@ -100,10 +96,6 @@ services:
|
||||
- app-node_modules:/usr/src/app/node_modules
|
||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||
- coverage:/usr/src/app/web/coverage
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
immich-server:
|
||||
@@ -135,7 +127,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:49214755b6153f90a597adcbff0252cc61069f8ab69ce8411285cd4a560e8038
|
||||
image: prom/prometheus@sha256:d936808bdea528155c0154a922cd42fd75716b8bb7ba302641350f9f3eaeba09
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
||||
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -24,6 +24,9 @@ server {
|
||||
# disable buffering uploads to prevent OOM on reverse proxy server and make uploads twice as fast (no pause)
|
||||
proxy_request_buffering off;
|
||||
|
||||
# increase body buffer to avoid limiting upload speed
|
||||
client_body_buffer_size 1024k;
|
||||
|
||||
# Set headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -32,8 +35,6 @@ server {
|
||||
|
||||
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
|
||||
# set timeout
|
||||
@@ -43,6 +44,8 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_pass http://<backend_url>:2283;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# useful when using Let's Encrypt http-01 challenge
|
||||
|
||||
@@ -52,7 +52,7 @@ Password login has been enabled.
|
||||
Disable Maintenance Mode
|
||||
|
||||
```
|
||||
immich-admin disable-maintenace-mode
|
||||
immich-admin disable-maintenance-mode
|
||||
Maintenance mode has been disabled.
|
||||
```
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
||||
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
||||
|
||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -36,7 +36,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"exiftool-vendored": "^34.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"backup_options_page_title": "Backup options",
|
||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||
"backup_settings_subtitle": "Manage upload settings",
|
||||
"backup_upload_details_page_more_details": "Tap for more details",
|
||||
"backward": "Backward",
|
||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||
@@ -718,6 +719,7 @@
|
||||
"check_corrupt_asset_backup_button": "Perform check",
|
||||
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
||||
"check_logs": "Check Logs",
|
||||
"checksum": "Checksum",
|
||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||
"city": "City",
|
||||
"clear": "Clear",
|
||||
@@ -1166,6 +1168,7 @@
|
||||
"header_settings_header_name_input": "Header name",
|
||||
"header_settings_header_value_input": "Header value",
|
||||
"headers_settings_tile_title": "Custom proxy headers",
|
||||
"height": "Height",
|
||||
"hi_user": "Hi {name} ({email})",
|
||||
"hide_all_people": "Hide all people",
|
||||
"hide_gallery": "Hide gallery",
|
||||
@@ -1288,6 +1291,7 @@
|
||||
"local": "Local",
|
||||
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
||||
"local_assets": "Local Assets",
|
||||
"local_id": "Local ID",
|
||||
"local_media_summary": "Local Media Summary",
|
||||
"local_network": "Local network",
|
||||
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
||||
@@ -2218,6 +2222,7 @@
|
||||
"week": "Week",
|
||||
"welcome": "Welcome",
|
||||
"welcome_to_immich": "Welcome to Immich",
|
||||
"width": "Width",
|
||||
"wifi_name": "Wi-Fi Name",
|
||||
"workflow": "Workflow",
|
||||
"wrong_pin_code": "Wrong PIN code",
|
||||
|
||||
@@ -137,8 +137,7 @@
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>We need local network permission to connect to the local server using IP address and
|
||||
allow the casting feature to work</string>
|
||||
<string>We need local network permission to connect to the local server using IP address and allow the casting feature to work</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
|
||||
@@ -98,7 +98,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Tap for more details',
|
||||
"backup_upload_details_page_more_details".t(context: context),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
@@ -239,14 +239,20 @@ class FileDetailDialog extends ConsumerWidget {
|
||||
const SizedBox(height: 24),
|
||||
if (asset != null) ...[
|
||||
_buildInfoSection(context, [
|
||||
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)),
|
||||
_buildInfoRow(context, "Local ID", asset.id),
|
||||
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)),
|
||||
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"),
|
||||
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"),
|
||||
_buildInfoRow(context, "Created At", asset.createdAt.toString()),
|
||||
_buildInfoRow(context, "Updated At", asset.updatedAt.toString()),
|
||||
if (asset.checksum != null) _buildInfoRow(context, "Checksum", asset.checksum!),
|
||||
_buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)),
|
||||
_buildInfoRow(context, "local_id".t(context: context), asset.id),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
"file_size".t(context: context),
|
||||
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||
),
|
||||
if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"),
|
||||
if (asset.height != null)
|
||||
_buildInfoRow(context, "height".t(context: context), "${asset.height}px"),
|
||||
_buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()),
|
||||
_buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()),
|
||||
if (asset.checksum != null)
|
||||
_buildInfoRow(context, "checksum".t(context: context), asset.checksum!),
|
||||
]),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final _features = [
|
||||
_Feature(
|
||||
name: 'Main Timeline',
|
||||
icon: Icons.timeline_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Selection Mode Timeline',
|
||||
icon: Icons.developer_mode_rounded,
|
||||
onTap: (ctx, ref) async {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
if (user == null) {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
final assets = await ref.read(remoteAssetRepositoryProvider).getSome(user.id);
|
||||
|
||||
final selectedAssets = await ctx.pushRoute<Set<BaseAsset>>(
|
||||
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: assets.toSet()),
|
||||
);
|
||||
|
||||
Logger("FeaturesInDevelopment").fine("Selected ${selectedAssets?.length ?? 0} assets");
|
||||
|
||||
return Future.value();
|
||||
},
|
||||
),
|
||||
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
|
||||
_Feature(
|
||||
name: 'Sync Local',
|
||||
icon: Icons.photo_album_rounded,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Sync Local Full (1)',
|
||||
icon: Icons.photo_library_rounded,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Hash Local Assets (2)',
|
||||
icon: Icons.numbers_outlined,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Sync Remote (3)',
|
||||
icon: Icons.refresh_rounded,
|
||||
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'WAL Checkpoint',
|
||||
icon: Icons.save_rounded,
|
||||
onTap: (_, ref) => ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"),
|
||||
),
|
||||
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
|
||||
_Feature(
|
||||
name: 'Clear Delta Checkpoint',
|
||||
icon: Icons.delete_rounded,
|
||||
onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Clear Local Data',
|
||||
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
|
||||
icon: Icons.delete_forever_rounded,
|
||||
onTap: (_, ref) async {
|
||||
final db = ref.read(driftProvider);
|
||||
await db.localAssetEntity.deleteAll();
|
||||
await db.localAlbumEntity.deleteAll();
|
||||
await db.localAlbumAssetEntity.deleteAll();
|
||||
},
|
||||
),
|
||||
_Feature(
|
||||
name: 'Clear Remote Data',
|
||||
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
|
||||
icon: Icons.delete_sweep_rounded,
|
||||
onTap: (_, ref) async {
|
||||
final db = ref.read(driftProvider);
|
||||
await db.remoteAssetEntity.deleteAll();
|
||||
await db.remoteExifEntity.deleteAll();
|
||||
await db.remoteAlbumEntity.deleteAll();
|
||||
await db.remoteAlbumUserEntity.deleteAll();
|
||||
await db.remoteAlbumAssetEntity.deleteAll();
|
||||
await db.memoryEntity.deleteAll();
|
||||
await db.memoryAssetEntity.deleteAll();
|
||||
await db.stackEntity.deleteAll();
|
||||
await db.personEntity.deleteAll();
|
||||
await db.assetFaceEntity.deleteAll();
|
||||
},
|
||||
),
|
||||
_Feature(
|
||||
name: 'Local Media Summary',
|
||||
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
|
||||
icon: Icons.table_chart_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Remote Media Summary',
|
||||
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
|
||||
icon: Icons.summarize_rounded,
|
||||
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
|
||||
),
|
||||
_Feature(
|
||||
name: 'Reset Sqlite',
|
||||
icon: Icons.table_view_rounded,
|
||||
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
onTap: (_, ref) async {
|
||||
final drift = ref.read(driftProvider);
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
final migrator = drift.createMigrator();
|
||||
for (final entity in drift.allSchemaEntities) {
|
||||
await migrator.drop(entity);
|
||||
await migrator.create(entity);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
class FeatInDevPage extends StatelessWidget {
|
||||
const FeatInDevPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('features_in_development'.tr()), centerTitle: true),
|
||||
body: Column(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final feat = _features[index];
|
||||
return Consumer(
|
||||
builder: (ctx, ref, _) => ListTile(
|
||||
title: Text(feat.name, style: feat.style),
|
||||
trailing: Icon(feat.icon),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onTap: () => unawaited(feat.onTap(ctx, ref)),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: _features.length,
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Feature {
|
||||
const _Feature({required this.name, required this.icon, required this.onTap, this.style});
|
||||
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final TextStyle? style;
|
||||
final Future<void> Function(BuildContext, WidgetRef _) onTap;
|
||||
}
|
||||
51
mobile/lib/presentation/pages/dev/ui_showcase.page.dart
Normal file
51
mobile/lib/presentation/pages/dev/ui_showcase.page.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
List<Widget> _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) {
|
||||
final children = <Widget>[];
|
||||
|
||||
final items = [
|
||||
(variant: ImmichVariant.filled, title: "Filled Variant"),
|
||||
(variant: ImmichVariant.ghost, title: "Ghost Variant"),
|
||||
];
|
||||
|
||||
for (final (:variant, :title) in items) {
|
||||
children.add(Text(title));
|
||||
children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)]));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class ImmichUIShowcasePage extends StatelessWidget {
|
||||
const ImmichUIShowcasePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Immich UI Showcase')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("IconButton", style: context.textTheme.titleLarge),
|
||||
..._showcaseBuilder(
|
||||
(variant, color) =>
|
||||
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onTap: () {}),
|
||||
),
|
||||
Text("CloseButton", style: context.textTheme.titleLarge),
|
||||
..._showcaseBuilder((variant, color) => ImmichCloseButton(color: color, variant: variant, onTap: () {})),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(album.name),
|
||||
actions: [const LikeActivityActionButton(menuItem: true)],
|
||||
actions: [const LikeActivityActionButton(iconOnly: true)],
|
||||
actionsPadding: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
body: activities.widgetWhen(
|
||||
|
||||
@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
||||
bool isAlbumTitleTextFieldFocus = false;
|
||||
Set<BaseAsset> selectedAssets = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
albumTitleController.addListener(_onTitleChanged);
|
||||
}
|
||||
|
||||
void _onTitleChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
albumTitleController.removeListener(_onTitleChanged);
|
||||
albumTitleController.dispose();
|
||||
albumDescriptionController.dispose();
|
||||
albumTitleTextFieldFocusNode.dispose();
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
/// A widget for cropping an image.
|
||||
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
|
||||
@@ -30,11 +31,13 @@ class DriftCropImagePage extends HookWidget {
|
||||
appBar: AppBar(
|
||||
backgroundColor: context.scaffoldBackgroundColor,
|
||||
title: Text("crop".tr()),
|
||||
leading: CloseButton(color: context.primaryColor),
|
||||
leading: const ImmichCloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
|
||||
onPressed: () async {
|
||||
ImmichIconButton(
|
||||
icon: Icons.done_rounded,
|
||||
color: ImmichColor.primary,
|
||||
variant: ImmichVariant.ghost,
|
||||
onTap: () async {
|
||||
final croppedImage = await cropController.croppedImage();
|
||||
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
|
||||
},
|
||||
@@ -72,17 +75,17 @@ class DriftCropImagePage extends HookWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color),
|
||||
onPressed: () {
|
||||
cropController.rotateLeft();
|
||||
},
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_left,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onTap: () => cropController.rotateLeft(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color),
|
||||
onPressed: () {
|
||||
cropController.rotateRight();
|
||||
},
|
||||
ImmichIconButton(
|
||||
icon: Icons.rotate_right,
|
||||
variant: ImmichVariant.ghost,
|
||||
color: ImmichColor.secondary,
|
||||
onTap: () => cropController.rotateRight(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -21,12 +21,36 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
||||
|
||||
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
||||
|
||||
class AddActionButton extends ConsumerWidget {
|
||||
const AddActionButton({super.key});
|
||||
class AddActionButton extends ConsumerStatefulWidget {
|
||||
const AddActionButton({super.key, this.originalTheme});
|
||||
|
||||
Future<void> _showAddOptions(BuildContext context, WidgetRef ref) async {
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
@override
|
||||
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
|
||||
}
|
||||
|
||||
class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
void _handleMenuSelection(AddToMenuItem selected) {
|
||||
switch (selected) {
|
||||
case AddToMenuItem.album:
|
||||
_openAlbumSelector();
|
||||
break;
|
||||
case AddToMenuItem.archive:
|
||||
performArchiveAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
case AddToMenuItem.unarchive:
|
||||
performUnArchiveAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
case AddToMenuItem.lockedFolder:
|
||||
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildMenuChildren() {
|
||||
final asset = ref.read(currentAssetNotifier);
|
||||
if (asset == null) return;
|
||||
if (asset == null) return [];
|
||||
|
||||
final user = ref.read(currentUserProvider);
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
@@ -35,84 +59,50 @@ class AddActionButton extends ConsumerWidget {
|
||||
final hasRemote = asset is RemoteAsset;
|
||||
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
||||
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
||||
final menuItemHeight = 30.0;
|
||||
|
||||
final List<PopupMenuEntry<AddToMenuItem>> items = [
|
||||
PopupMenuItem(
|
||||
enabled: false,
|
||||
textStyle: context.textTheme.labelMedium,
|
||||
height: 40,
|
||||
child: Text("add_to_bottom_bar".tr()),
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
|
||||
),
|
||||
PopupMenuItem(
|
||||
height: menuItemHeight,
|
||||
value: AddToMenuItem.album,
|
||||
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("album".tr())),
|
||||
BaseActionButton(
|
||||
iconData: Icons.photo_album_outlined,
|
||||
label: "album".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.album),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(enabled: false, textStyle: context.textTheme.labelMedium, height: 40, child: Text("move_to".tr())),
|
||||
|
||||
if (isOwner) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||
),
|
||||
if (showArchive)
|
||||
PopupMenuItem(
|
||||
height: menuItemHeight,
|
||||
value: AddToMenuItem.archive,
|
||||
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())),
|
||||
BaseActionButton(
|
||||
iconData: Icons.archive_outlined,
|
||||
label: "archive".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
|
||||
),
|
||||
if (showUnarchive)
|
||||
PopupMenuItem(
|
||||
height: menuItemHeight,
|
||||
value: AddToMenuItem.unarchive,
|
||||
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())),
|
||||
BaseActionButton(
|
||||
iconData: Icons.unarchive_outlined,
|
||||
label: "unarchive".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
|
||||
),
|
||||
PopupMenuItem(
|
||||
height: menuItemHeight,
|
||||
value: AddToMenuItem.lockedFolder,
|
||||
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())),
|
||||
BaseActionButton(
|
||||
iconData: Icons.lock_outline,
|
||||
label: "locked_folder".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
final AddToMenuItem? selected = await showMenu<AddToMenuItem>(
|
||||
context: context,
|
||||
color: context.themeData.scaffoldBackgroundColor,
|
||||
position: _menuPosition(context),
|
||||
items: items,
|
||||
popUpAnimationStyle: AnimationStyle.noAnimation,
|
||||
);
|
||||
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (selected) {
|
||||
case AddToMenuItem.album:
|
||||
_openAlbumSelector(context, ref);
|
||||
break;
|
||||
case AddToMenuItem.archive:
|
||||
await performArchiveAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
case AddToMenuItem.unarchive:
|
||||
await performUnArchiveAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
case AddToMenuItem.lockedFolder:
|
||||
await performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RelativeRect _menuPosition(BuildContext context) {
|
||||
final renderObject = context.findRenderObject();
|
||||
if (renderObject is! RenderBox) {
|
||||
return RelativeRect.fill;
|
||||
}
|
||||
|
||||
final size = renderObject.size;
|
||||
final position = renderObject.localToGlobal(Offset.zero);
|
||||
|
||||
return RelativeRect.fromLTRB(position.dx, position.dy - size.height - 200, position.dx + size.width, position.dy);
|
||||
}
|
||||
|
||||
void _openAlbumSelector(BuildContext context, WidgetRef ref) {
|
||||
void _openAlbumSelector() {
|
||||
final currentAsset = ref.read(currentAssetNotifier);
|
||||
if (currentAsset == null) {
|
||||
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
||||
@@ -120,7 +110,8 @@ class AddActionButton extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final List<Widget> slivers = [
|
||||
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)),
|
||||
const CreateAlbumButton(),
|
||||
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)),
|
||||
];
|
||||
|
||||
showModalBottomSheet(
|
||||
@@ -141,7 +132,7 @@ class AddActionButton extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async {
|
||||
Future<void> _addCurrentAssetToAlbum(RemoteAlbum album) async {
|
||||
final latest = ref.read(currentAssetNotifier);
|
||||
|
||||
if (latest == null) {
|
||||
@@ -165,6 +156,9 @@ class AddActionButton extends ConsumerWidget {
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||
}
|
||||
|
||||
if (!context.mounted) {
|
||||
@@ -174,17 +168,38 @@ class AddActionButton extends ConsumerWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
if (asset == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Builder(
|
||||
builder: (buttonContext) {
|
||||
|
||||
final themeData = widget.originalTheme ?? context.themeData;
|
||||
|
||||
return MenuAnchor(
|
||||
consumeOutsideTap: true,
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
|
||||
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
|
||||
elevation: const WidgetStatePropertyAll(4),
|
||||
shape: const WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||
),
|
||||
menuChildren: widget.originalTheme != null
|
||||
? [
|
||||
Theme(
|
||||
data: widget.originalTheme!,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _buildMenuChildren()),
|
||||
),
|
||||
]
|
||||
: _buildMenuChildren(),
|
||||
builder: (context, controller, child) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.add,
|
||||
label: "add_to_bottom_bar".tr(),
|
||||
onPressed: () => _showAddOptions(buttonContext, ref),
|
||||
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -9,8 +9,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
class AdvancedInfoActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const AdvancedInfoActionButton({super.key, required this.source});
|
||||
const AdvancedInfoActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -26,6 +28,8 @@ class AdvancedInfoActionButton extends ConsumerWidget {
|
||||
maxWidth: 115.0,
|
||||
iconData: Icons.help_outline_rounded,
|
||||
label: "troubleshoot".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required
|
||||
|
||||
class ArchiveActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const ArchiveActionButton({super.key, required this.source});
|
||||
const ArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
await performArchiveAction(context, ref, source: source);
|
||||
@@ -47,6 +49,8 @@ class ArchiveActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.archive_outlined,
|
||||
label: "to_archive".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class BaseActionButton extends StatelessWidget {
|
||||
class BaseActionButton extends ConsumerWidget {
|
||||
const BaseActionButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
@@ -11,6 +12,7 @@ class BaseActionButton extends StatelessWidget {
|
||||
this.onLongPressed,
|
||||
this.maxWidth = 90.0,
|
||||
this.minWidth,
|
||||
this.iconOnly = false,
|
||||
this.menuItem = false,
|
||||
});
|
||||
|
||||
@@ -19,25 +21,42 @@ class BaseActionButton extends StatelessWidget {
|
||||
final Color? iconColor;
|
||||
final double maxWidth;
|
||||
final double? minWidth;
|
||||
|
||||
/// When true, renders only an IconButton without text label
|
||||
final bool iconOnly;
|
||||
|
||||
/// When true, renders as a MenuItemButton for use in MenuAnchor menus
|
||||
final bool menuItem;
|
||||
final void Function()? onPressed;
|
||||
final void Function()? onLongPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||
final iconTheme = IconTheme.of(context);
|
||||
final iconSize = iconTheme.size ?? 24.0;
|
||||
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
|
||||
final textColor = context.themeData.textTheme.labelLarge?.color;
|
||||
|
||||
if (menuItem) {
|
||||
if (iconOnly) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(iconData, size: iconSize, color: iconColor),
|
||||
);
|
||||
}
|
||||
|
||||
if (menuItem) {
|
||||
final theme = context.themeData;
|
||||
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
||||
|
||||
return MenuItemButton(
|
||||
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
||||
leadingIcon: Icon(iconData, color: effectiveIconColor),
|
||||
onPressed: onPressed,
|
||||
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)),
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: MaterialButton(
|
||||
|
||||
@@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||
|
||||
class CastActionButton extends ConsumerWidget {
|
||||
const CastActionButton({super.key, this.menuItem = true});
|
||||
const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
@override
|
||||
@@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget {
|
||||
onPressed: () {
|
||||
showDialog(context: context, builder: (context) => const CastDialog());
|
||||
},
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,15 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
class DeleteActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool showConfirmation;
|
||||
const DeleteActionButton({super.key, required this.source, this.showConfirmation = false});
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
const DeleteActionButton({
|
||||
super.key,
|
||||
required this.source,
|
||||
this.showConfirmation = false,
|
||||
this.iconOnly = false,
|
||||
this.menuItem = false,
|
||||
});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -74,6 +82,8 @@ class DeleteActionButton extends ConsumerWidget {
|
||||
maxWidth: 110.0,
|
||||
iconData: Icons.delete_sweep_outlined,
|
||||
label: "delete".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
/// - Prompt to delete the asset locally
|
||||
class DeleteLocalActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const DeleteLocalActionButton({super.key, required this.source});
|
||||
const DeleteLocalActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -55,6 +57,8 @@ class DeleteLocalActionButton extends ConsumerWidget {
|
||||
maxWidth: 95.0,
|
||||
iconData: Icons.no_cell_outlined,
|
||||
label: "control_bottom_app_bar_delete_from_local".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
/// - Prompt to delete the asset locally
|
||||
class DeletePermanentActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const DeletePermanentActionButton({super.key, required this.source});
|
||||
const DeletePermanentActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -51,6 +53,8 @@ class DeletePermanentActionButton extends ConsumerWidget {
|
||||
maxWidth: 110.0,
|
||||
iconData: Icons.delete_forever,
|
||||
label: "delete_permanently".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class DownloadActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
const DownloadActionButton({super.key, required this.source, this.menuItem = false});
|
||||
const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
|
||||
if (!context.mounted) {
|
||||
@@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget {
|
||||
iconData: Icons.download,
|
||||
maxWidth: 95,
|
||||
label: "download".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref, backgroundManager),
|
||||
);
|
||||
|
||||
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class FavoriteActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const FavoriteActionButton({super.key, required this.source, this.menuItem = false});
|
||||
const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.favorite_border_rounded,
|
||||
label: "favorite".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
|
||||
@@ -12,8 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
class LikeActivityActionButton extends ConsumerWidget {
|
||||
const LikeActivityActionButton({super.key, this.menuItem = false});
|
||||
const LikeActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
@override
|
||||
@@ -46,17 +47,19 @@ class LikeActivityActionButton extends ConsumerWidget {
|
||||
|
||||
return BaseActionButton(
|
||||
maxWidth: 60,
|
||||
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
|
||||
iconData: liked != null ? Icons.thumb_up : Icons.thumb_up_off_alt,
|
||||
label: "like".t(context: context),
|
||||
onPressed: () => onTap(liked),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
},
|
||||
|
||||
// default to empty heart during loading
|
||||
loading: () => BaseActionButton(
|
||||
iconData: Icons.favorite_border,
|
||||
iconData: Icons.thumb_up_off_alt,
|
||||
label: "like".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
|
||||
|
||||
@@ -5,8 +5,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
|
||||
class MotionPhotoActionButton extends ConsumerWidget {
|
||||
const MotionPhotoActionButton({super.key, this.menuItem = true});
|
||||
const MotionPhotoActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
@override
|
||||
@@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
|
||||
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
|
||||
label: "play_motion_photo".t(context: context),
|
||||
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,8 +38,10 @@ Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref,
|
||||
|
||||
class MoveToLockFolderActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const MoveToLockFolderActionButton({super.key, required this.source});
|
||||
const MoveToLockFolderActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
await performMoveToLockFolderAction(context, ref, source: source);
|
||||
@@ -51,6 +53,8 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
||||
maxWidth: 115.0,
|
||||
iconData: Icons.lock_outline_rounded,
|
||||
label: "move_to_locked_folder".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
@@ -11,8 +13,16 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
class RemoveFromAlbumActionButton extends ConsumerWidget {
|
||||
final String albumId;
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const RemoveFromAlbumActionButton({super.key, required this.albumId, required this.source});
|
||||
const RemoveFromAlbumActionButton({
|
||||
super.key,
|
||||
required this.albumId,
|
||||
required this.source,
|
||||
this.iconOnly = false,
|
||||
this.menuItem = false,
|
||||
});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -22,6 +32,10 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
|
||||
final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId);
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
if (source == ActionSource.viewer) {
|
||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||
}
|
||||
|
||||
final successMessage = 'remove_from_album_action_prompt'.t(
|
||||
context: context,
|
||||
args: {'count': result.count.toString()},
|
||||
@@ -42,6 +56,8 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.remove_circle_outline,
|
||||
label: "remove_from_album".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
maxWidth: 100,
|
||||
);
|
||||
|
||||
@@ -10,8 +10,15 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const RemoveFromLockFolderActionButton({super.key, required this.source});
|
||||
const RemoveFromLockFolderActionButton({
|
||||
super.key,
|
||||
required this.source,
|
||||
this.iconOnly = false,
|
||||
this.menuItem = false,
|
||||
});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -42,6 +49,8 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
||||
maxWidth: 100.0,
|
||||
iconData: Icons.lock_open_rounded,
|
||||
label: "remove_from_locked_folder".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@ class _SharePreparingDialog extends StatelessWidget {
|
||||
|
||||
class ShareActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const ShareActionButton({super.key, required this.source});
|
||||
const ShareActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -74,6 +76,8 @@ class ShareActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||
label: 'share'.t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
class ShareLinkActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const ShareLinkActionButton({super.key, required this.source});
|
||||
const ShareLinkActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
_onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -23,6 +25,8 @@ class ShareLinkActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.link_rounded,
|
||||
label: "share_link".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
final String assetId;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const SimilarPhotosActionButton({super.key, required this.assetId});
|
||||
const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -44,6 +46,8 @@ class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.compare,
|
||||
label: "view_similar_photos".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
maxWidth: 100,
|
||||
);
|
||||
|
||||
@@ -15,8 +15,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
/// which will be permanently deleted after the number of days configure by the admin
|
||||
class TrashActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const TrashActionButton({super.key, required this.source});
|
||||
const TrashActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -48,6 +50,8 @@ class TrashActionButton extends ConsumerWidget {
|
||||
maxWidth: 85.0,
|
||||
iconData: Icons.delete_outline_rounded,
|
||||
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,10 @@ Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {requir
|
||||
|
||||
class UnArchiveActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const UnArchiveActionButton({super.key, required this.source});
|
||||
const UnArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
await performUnArchiveAction(context, ref, source: source);
|
||||
@@ -49,6 +51,8 @@ class UnArchiveActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.unarchive_outlined,
|
||||
label: "unarchive".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class UnFavoriteActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false});
|
||||
const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
|
||||
iconData: Icons.favorite_rounded,
|
||||
label: "unfavorite".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class UnStackActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const UnStackActionButton({super.key, required this.source});
|
||||
const UnStackActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -38,6 +40,8 @@ class UnStackActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.layers_clear_outlined,
|
||||
label: "unstack".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class UploadActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const UploadActionButton({super.key, required this.source});
|
||||
const UploadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
@@ -39,6 +41,8 @@ class UploadActionButton extends ConsumerWidget {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.backup_outlined,
|
||||
label: "upload".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
@@ -766,3 +768,68 @@ class AddToAlbumHeader extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateAlbumButton extends ConsumerWidget {
|
||||
const CreateAlbumButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Future<void> onCreateAlbum() async {
|
||||
var albumName = await showDialog<String?>(context: context, builder: (context) => const NewAlbumNameModal());
|
||||
if (albumName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final asset = ref.read(currentAssetNotifier);
|
||||
|
||||
if (asset == null) {
|
||||
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
final album = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.createAlbum(title: albumName, assetIds: [asset.remoteId!]);
|
||||
|
||||
if (album == null) {
|
||||
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
|
||||
return;
|
||||
}
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
ref.invalidate(albumsContainingAssetProvider(asset.remoteId!));
|
||||
|
||||
context.pop();
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("add_to_album", style: context.textTheme.titleSmall).tr(),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: const Size(0, 0),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: onCreateAlbum,
|
||||
icon: Icon(Icons.add, color: context.primaryColor),
|
||||
label: Text(
|
||||
"common_create_new_album",
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class NewAlbumNameModal extends StatefulWidget {
|
||||
const NewAlbumNameModal({super.key});
|
||||
|
||||
@override
|
||||
State<NewAlbumNameModal> createState() => _NewAlbumNameModalState();
|
||||
}
|
||||
|
||||
class _NewAlbumNameModalState extends State<NewAlbumNameModal> {
|
||||
TextEditingController nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("album_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
||||
content: SingleChildScrollView(
|
||||
child: TextFormField(
|
||||
controller: nameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(null),
|
||||
child: Text(
|
||||
"cancel",
|
||||
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop(nameController.text.trim());
|
||||
},
|
||||
child: Text(
|
||||
"create_album",
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class ActivitiesBottomSheet extends HookConsumerWidget {
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
backgroundColor: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class AssetViewer extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
const double _kBottomSheetMinimumExtent = 0.4;
|
||||
const double _kBottomSheetSnapExtent = 0.7;
|
||||
const double _kBottomSheetSnapExtent = 0.67;
|
||||
|
||||
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
static final _dummyListener = ImageStreamListener((image, _) => image.dispose());
|
||||
@@ -399,10 +399,14 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
final isDraggingDown = currentExtent < previousExtent;
|
||||
previousExtent = currentExtent;
|
||||
// Closes the bottom sheet if the user is dragging down
|
||||
if (isDraggingDown && delta.extent < 0.55) {
|
||||
if (isDraggingDown && delta.extent < 0.67) {
|
||||
if (dragInProgress) {
|
||||
blockGestures = true;
|
||||
}
|
||||
// Jump to a lower position before starting close animation to prevent glitch
|
||||
if (bottomSheetController.isAttached) {
|
||||
bottomSheetController.jumpTo(0.67);
|
||||
}
|
||||
sheetCloseController?.close();
|
||||
}
|
||||
|
||||
@@ -480,7 +484,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
previousExtent = _kBottomSheetMinimumExtent;
|
||||
sheetCloseController = showBottomSheet(
|
||||
context: ctx,
|
||||
sheetAnimationStyle: const AnimationStyle(duration: Durations.short4, reverseDuration: Durations.short2),
|
||||
sheetAnimationStyle: const AnimationStyle(duration: Durations.medium2, reverseDuration: Durations.medium2),
|
||||
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
|
||||
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
|
||||
@@ -688,16 +692,20 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||
enablePanAlways: true,
|
||||
),
|
||||
if (!showingBottomSheet)
|
||||
const Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [AssetStackRow(), ViewerBottomBar()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: showingBottomSheet
|
||||
? const SizedBox.shrink()
|
||||
: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [AssetStackRow(), ViewerBottomBar()],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,16 +38,21 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
opacity = 0;
|
||||
}
|
||||
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final actions = <Widget>[
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.hasRemote) const AddActionButton(),
|
||||
|
||||
if (isOwner) ...[
|
||||
asset.isLocalOnly
|
||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
if (!isInLockedView) ...[
|
||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||
|
||||
if (isOwner) ...[
|
||||
asset.isLocalOnly
|
||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -74,7 +79,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (asset.isVideo) const VideoControls(),
|
||||
if (!isInLockedView && !isReadonlyModeEnabled)
|
||||
if (!isReadonlyModeEnabled)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
@@ -21,14 +20,9 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -48,29 +42,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == ref.watch(currentUserProvider)?.id;
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: isOwner,
|
||||
isArchived: isArchived,
|
||||
isTrashEnabled: isTrashEnable,
|
||||
isInLockedView: isInLockedView,
|
||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||
currentAlbum: currentAlbum,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final actions = ActionButtonBuilder.build(buttonContext);
|
||||
|
||||
return BaseBottomSheet(
|
||||
actions: actions,
|
||||
actions: [],
|
||||
slivers: const [_AssetDetailBottomSheet()],
|
||||
controller: controller,
|
||||
initialChildSize: initialChildSize,
|
||||
@@ -79,7 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
backgroundColor: context.isDarkTheme ? context.colorScheme.surface : Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -326,7 +299,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
// Appears in (Albums)
|
||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||
// padding at the bottom to avoid cut-off
|
||||
const SizedBox(height: 100),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,25 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
const ViewerTopAppBar({super.key});
|
||||
@@ -41,15 +35,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final showViewInTimelineButton =
|
||||
timelineOrigin != TimelineOrigin.main &&
|
||||
timelineOrigin != TimelineOrigin.deepLink &&
|
||||
timelineOrigin != TimelineOrigin.trash &&
|
||||
timelineOrigin != TimelineOrigin.archive &&
|
||||
timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
isOwner;
|
||||
|
||||
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
@@ -62,11 +47,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
opacity = 0;
|
||||
}
|
||||
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final originalTheme = context.themeData;
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
@@ -74,28 +58,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
|
||||
},
|
||||
),
|
||||
if (showViewInTimelineButton)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.maybePop();
|
||||
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
||||
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
|
||||
},
|
||||
icon: const Icon(Icons.image_search),
|
||||
tooltip: 'view_in_timeline'.t(context: context),
|
||||
),
|
||||
|
||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
||||
const _KebabMenu(),
|
||||
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||
|
||||
ViewerKebabMenu(originalTheme: originalTheme),
|
||||
];
|
||||
|
||||
final lockedViewActions = <Widget>[
|
||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
||||
const _KebabMenu(),
|
||||
];
|
||||
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: opacity < 255,
|
||||
@@ -122,20 +94,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
Size get preferredSize => const Size.fromHeight(60.0);
|
||||
}
|
||||
|
||||
class _KebabMenu extends ConsumerWidget {
|
||||
const _KebabMenu();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
|
||||
},
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarBackButton extends ConsumerWidget {
|
||||
const _AppBarBackButton();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
|
||||
@@ -104,7 +105,12 @@ class NativeVideoViewer extends HookConsumerWidget {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
|
||||
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file);
|
||||
// Pass a file:// URI so Android's Uri.parse doesn't
|
||||
// interpret characters like '#' as fragment identifiers.
|
||||
final source = await VideoSource.init(
|
||||
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.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/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
class ViewerKebabMenu extends ConsumerWidget {
|
||||
const ViewerKebabMenu({super.key, this.originalTheme});
|
||||
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
if (asset == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final user = ref.watch(currentUserProvider);
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
final actionContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
isOwner: isOwner,
|
||||
isArchived: isArchived,
|
||||
isTrashEnabled: isTrashEnable,
|
||||
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: currentAlbum,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
isCasting: isCasting,
|
||||
timelineOrigin: timelineOrigin,
|
||||
originalTheme: originalTheme,
|
||||
);
|
||||
|
||||
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
|
||||
|
||||
return MenuAnchor(
|
||||
consumeOutsideTap: true,
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
||||
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
|
||||
elevation: const WidgetStatePropertyAll(4),
|
||||
shape: const WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||
),
|
||||
menuChildren: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 150),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: menuChildren,
|
||||
),
|
||||
),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.more_vert_rounded),
|
||||
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -47,10 +46,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
const SliverPersistentHeader(delegate: _DragHandleDelegate(), pinned: true),
|
||||
const SliverToBoxAdapter(child: _DragHandle()),
|
||||
if (widget.actions.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
@@ -108,31 +108,13 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
||||
}
|
||||
}
|
||||
|
||||
class _DragHandleDelegate extends SliverPersistentHeaderDelegate {
|
||||
const _DragHandleDelegate();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return const _DragHandle();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_DragHandleDelegate oldDelegate) => false;
|
||||
|
||||
@override
|
||||
double get minExtent => 50.0;
|
||||
|
||||
@override
|
||||
double get maxExtent => 50.0;
|
||||
}
|
||||
|
||||
class _DragHandle extends StatelessWidget {
|
||||
const _DragHandle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 50,
|
||||
height: 38,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
|
||||
@@ -17,7 +17,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -86,10 +85,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
slivers: multiselect.hasRemote
|
||||
? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)]
|
||||
|
||||
@@ -18,7 +18,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -112,10 +111,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
if (multiselect.hasLocal) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: ownsAlbum
|
||||
|
||||
@@ -324,7 +324,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
|
||||
|
||||
const scrubberBottomPadding = 100.0;
|
||||
final bottomPadding = context.padding.bottom + (widget.appBar == null ? 0 : scrubberBottomPadding);
|
||||
const bottomSheetOpenModifier = 120.0;
|
||||
final bottomPadding =
|
||||
context.padding.bottom +
|
||||
(widget.appBar == null ? 0 : scrubberBottomPadding) +
|
||||
(isMultiSelectEnabled ? bottomSheetOpenModifier : 0);
|
||||
|
||||
final grid = CustomScrollView(
|
||||
primary: true,
|
||||
@@ -347,7 +351,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
addRepaintBoundaries: false,
|
||||
),
|
||||
),
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ class PersonApiRepository extends ApiRepository {
|
||||
}
|
||||
|
||||
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
||||
final dto = await checkNull(_api.updatePerson(id, PersonUpdateDto(name: name, birthDate: birthday)));
|
||||
return _toPerson(dto);
|
||||
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
|
||||
final dto = PersonUpdateDto(name: name, birthDate: birthdayUtc);
|
||||
final response = await checkNull(_api.updatePerson(id, dto));
|
||||
return _toPerson(response);
|
||||
}
|
||||
|
||||
static PersonDto _toPerson(PersonResponseDto dto) => PersonDto(
|
||||
|
||||
@@ -78,9 +78,9 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||
import 'package:immich_mobile/pages/settings/sync_status.page.dart';
|
||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/ui_showcase.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/download_info.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||
@@ -167,7 +167,7 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: ChangePasswordRoute.page),
|
||||
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
|
||||
CustomRoute(
|
||||
AutoRoute(
|
||||
page: TabControllerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
children: [
|
||||
@@ -176,9 +176,8 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
],
|
||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||
),
|
||||
CustomRoute(
|
||||
AutoRoute(
|
||||
page: TabShellRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
children: [
|
||||
@@ -187,7 +186,6 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
],
|
||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||
),
|
||||
CustomRoute(
|
||||
page: GalleryViewerRoute.page,
|
||||
@@ -288,7 +286,6 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
|
||||
AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: FeatInDevRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftBackupRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
@@ -340,6 +337,7 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: ImmichUIShowcaseRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
// required to handle all deeplinks in deep_link.service.dart
|
||||
// auto_route_library#1722
|
||||
RedirectRoute(path: '*', redirectTo: '/'),
|
||||
|
||||
@@ -1648,22 +1648,6 @@ class FavoritesRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [FeatInDevPage]
|
||||
class FeatInDevRoute extends PageRouteInfo<void> {
|
||||
const FeatInDevRoute({List<PageRouteInfo>? children})
|
||||
: super(FeatInDevRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'FeatInDevRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const FeatInDevPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [FilterImagePage]
|
||||
class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
|
||||
@@ -1831,6 +1815,22 @@ class HeaderSettingsRoute extends PageRouteInfo<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [ImmichUIShowcasePage]
|
||||
class ImmichUIShowcaseRoute extends PageRouteInfo<void> {
|
||||
const ImmichUIShowcaseRoute({List<PageRouteInfo>? children})
|
||||
: super(ImmichUIShowcaseRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ImmichUIShowcaseRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const ImmichUIShowcasePage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryPage]
|
||||
class LibraryRoute extends PageRouteInfo<void> {
|
||||
|
||||
@@ -27,7 +27,7 @@ enum AppSettingsEnum<T> {
|
||||
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
|
||||
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
|
||||
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
|
||||
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 0),
|
||||
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
@@ -42,7 +42,7 @@ enum AppSettingsEnum<T> {
|
||||
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
|
||||
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
|
||||
ignoreIcloudAssets<bool>(StoreKey.ignoreIcloudAssets, null, false),
|
||||
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, false),
|
||||
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, true),
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
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/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
@@ -19,6 +27,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class ActionButtonContext {
|
||||
final BaseAsset asset;
|
||||
@@ -30,6 +39,9 @@ class ActionButtonContext {
|
||||
final RemoteAlbum? currentAlbum;
|
||||
final bool advancedTroubleshooting;
|
||||
final ActionSource source;
|
||||
final bool isCasting;
|
||||
final TimelineOrigin timelineOrigin;
|
||||
final ThemeData? originalTheme;
|
||||
|
||||
const ActionButtonContext({
|
||||
required this.asset,
|
||||
@@ -41,27 +53,33 @@ class ActionButtonContext {
|
||||
required this.currentAlbum,
|
||||
required this.advancedTroubleshooting,
|
||||
required this.source,
|
||||
this.isCasting = false,
|
||||
this.timelineOrigin = TimelineOrigin.main,
|
||||
this.originalTheme,
|
||||
});
|
||||
}
|
||||
|
||||
enum ActionButtonType {
|
||||
advancedInfo,
|
||||
openInfo,
|
||||
likeActivity,
|
||||
share,
|
||||
shareLink,
|
||||
cast,
|
||||
similarPhotos,
|
||||
viewInTimeline,
|
||||
download,
|
||||
upload,
|
||||
unstack,
|
||||
archive,
|
||||
unarchive,
|
||||
download,
|
||||
trash,
|
||||
deletePermanent,
|
||||
delete,
|
||||
moveToLockFolder,
|
||||
removeFromLockFolder,
|
||||
deleteLocal,
|
||||
upload,
|
||||
removeFromAlbum,
|
||||
unstack,
|
||||
likeActivity;
|
||||
trash,
|
||||
deleteLocal,
|
||||
deletePermanent,
|
||||
delete,
|
||||
advancedInfo;
|
||||
|
||||
bool shouldShow(ActionButtonContext context) {
|
||||
return switch (this) {
|
||||
@@ -128,39 +146,165 @@ enum ActionButtonType {
|
||||
ActionButtonType.similarPhotos =>
|
||||
!context.isInLockedView && //
|
||||
context.asset is RemoteAsset,
|
||||
ActionButtonType.openInfo => true,
|
||||
ActionButtonType.viewInTimeline =>
|
||||
context.timelineOrigin != TimelineOrigin.main &&
|
||||
context.timelineOrigin != TimelineOrigin.deepLink &&
|
||||
context.timelineOrigin != TimelineOrigin.trash &&
|
||||
context.timelineOrigin != TimelineOrigin.lockedFolder &&
|
||||
context.timelineOrigin != TimelineOrigin.archive &&
|
||||
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||
context.isOwner,
|
||||
ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||
};
|
||||
}
|
||||
|
||||
Widget buildButton(ActionButtonContext context) {
|
||||
ConsumerWidget buildButton(
|
||||
ActionButtonContext context, [
|
||||
BuildContext? buildContext,
|
||||
bool iconOnly = false,
|
||||
bool menuItem = false,
|
||||
]) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
|
||||
ActionButtonType.archive => ArchiveActionButton(source: context.source),
|
||||
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
|
||||
ActionButtonType.download => DownloadActionButton(source: context.source),
|
||||
ActionButtonType.trash => TrashActionButton(source: context.source),
|
||||
ActionButtonType.deletePermanent => DeletePermanentActionButton(source: context.source),
|
||||
ActionButtonType.delete => DeleteActionButton(source: context.source),
|
||||
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(source: context.source),
|
||||
ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(source: context.source),
|
||||
ActionButtonType.deleteLocal => DeleteLocalActionButton(source: context.source),
|
||||
ActionButtonType.upload => UploadActionButton(source: context.source),
|
||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.unarchive => UnArchiveActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.deletePermanent => DeletePermanentActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.deleteLocal => DeleteLocalActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton(
|
||||
albumId: context.currentAlbum!.id,
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.likeActivity => const LikeActivityActionButton(),
|
||||
ActionButtonType.unstack => UnStackActionButton(source: context.source),
|
||||
ActionButtonType.similarPhotos => SimilarPhotosActionButton(assetId: (context.asset as RemoteAsset).id),
|
||||
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
|
||||
assetId: (context.asset as RemoteAsset).id,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.openInfo => BaseActionButton(
|
||||
label: 'info'.tr(),
|
||||
iconData: Icons.info_outline,
|
||||
iconColor: context.originalTheme?.iconTheme.color,
|
||||
menuItem: true,
|
||||
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
||||
),
|
||||
ActionButtonType.viewInTimeline => BaseActionButton(
|
||||
label: 'view_in_timeline'.tr(),
|
||||
iconData: Icons.image_search,
|
||||
iconColor: context.originalTheme?.iconTheme.color,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: buildContext == null
|
||||
? null
|
||||
: () async {
|
||||
await buildContext.maybePop();
|
||||
await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
||||
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
|
||||
},
|
||||
),
|
||||
ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
};
|
||||
}
|
||||
|
||||
/// Defines which group each button belongs to for kebab menu.
|
||||
/// Buttons in the same group will be displayed together,
|
||||
/// with dividers separating different groups.
|
||||
int get kebabMenuGroup => switch (this) {
|
||||
// 0: info
|
||||
ActionButtonType.openInfo => 0,
|
||||
// 10: move,remove, and delete
|
||||
ActionButtonType.trash => 10,
|
||||
ActionButtonType.deletePermanent => 10,
|
||||
ActionButtonType.removeFromLockFolder => 10,
|
||||
ActionButtonType.removeFromAlbum => 10,
|
||||
ActionButtonType.unstack => 10,
|
||||
ActionButtonType.archive => 10,
|
||||
ActionButtonType.unarchive => 10,
|
||||
ActionButtonType.moveToLockFolder => 10,
|
||||
ActionButtonType.deleteLocal => 10,
|
||||
ActionButtonType.delete => 10,
|
||||
// 90: advancedInfo
|
||||
ActionButtonType.advancedInfo => 90,
|
||||
// 1: others
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
|
||||
class ActionButtonBuilder {
|
||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||
static const List<ActionButtonType> defaultViewerKebabMenuOrder = _actionTypes;
|
||||
static const Set<ActionButtonType> defaultViewerBottomBarButtons = {
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.moveToLockFolder,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.unarchive,
|
||||
};
|
||||
|
||||
static List<Widget> build(ActionButtonContext context) {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
|
||||
static List<Widget> buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||
final visibleButtons = defaultViewerKebabMenuOrder
|
||||
.where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context))
|
||||
.toList();
|
||||
|
||||
if (visibleButtons.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<Widget> result = [];
|
||||
int? lastGroup;
|
||||
|
||||
for (final type in visibleButtons) {
|
||||
if (lastGroup != null && type.kebabMenuGroup != lastGroup) {
|
||||
result.add(const Divider(height: 1));
|
||||
}
|
||||
result.add(type.buildButton(context, buildContext, false, true).build(buildContext, ref));
|
||||
lastGroup = type.kebabMenuGroup;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
@@ -68,11 +69,11 @@ class ActivityTextField extends HookConsumerWidget {
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: IconButton(
|
||||
icon: Icon(liked ? Icons.favorite_rounded : Icons.favorite_border_rounded),
|
||||
icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt),
|
||||
onPressed: liked ? removeLike : addLike,
|
||||
),
|
||||
),
|
||||
suffixIconColor: liked ? Colors.red[700] : null,
|
||||
suffixIconColor: liked ? context.primaryColor : null,
|
||||
hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(),
|
||||
hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
|
||||
@@ -37,7 +37,7 @@ class ActivityTile extends HookConsumerWidget {
|
||||
? Container(
|
||||
width: isBottomSheet ? 30 : 44,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.favorite_rounded, color: Colors.red[700]),
|
||||
child: Icon(Icons.thumb_up, color: context.primaryColor),
|
||||
)
|
||||
: isBottomSheet
|
||||
? UserCircleAvatar(user: activity.user, size: 30, radius: 15)
|
||||
|
||||
@@ -67,8 +67,8 @@ class CommentBubble extends ConsumerWidget {
|
||||
bottom: 6,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
|
||||
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
|
||||
decoration: BoxDecoration(color: context.colorScheme.surfaceContainer, shape: BoxShape.circle),
|
||||
child: Icon(Icons.thumb_up, color: context.primaryColor, size: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -81,8 +81,8 @@ class CommentBubble extends ConsumerWidget {
|
||||
if (isLike && !showThumbnail) {
|
||||
likes = Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
|
||||
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
|
||||
decoration: BoxDecoration(color: context.colorScheme.surfaceContainer, shape: BoxShape.circle),
|
||||
child: Icon(Icons.thumb_up, color: context.primaryColor, size: 18),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,16 +53,18 @@ class ServerUpdateNotification extends HookConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
serverInfoState.versionStatus.message,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelLarge,
|
||||
Expanded(
|
||||
child: Text(
|
||||
serverInfoState.versionStatus.message,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
|
||||
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
|
||||
const Spacer(),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: openUpdateLink,
|
||||
style: TextButton.styleFrom(
|
||||
|
||||
@@ -155,8 +155,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||
if (kDebugMode || kProfileMode)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.science_rounded),
|
||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||
icon: const Icon(Icons.palette_rounded),
|
||||
onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
|
||||
),
|
||||
if (isCasting)
|
||||
Padding(
|
||||
|
||||
@@ -74,8 +74,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
||||
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||
if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.science_rounded),
|
||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
||||
icon: const Icon(Icons.palette_rounded),
|
||||
onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
|
||||
),
|
||||
if (showUploadButton && !isReadonlyModeEnabled)
|
||||
const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),
|
||||
|
||||
@@ -2,26 +2,27 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class EntitiyCountTile extends StatelessWidget {
|
||||
class EntityCountTile extends StatelessWidget {
|
||||
final int count;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
|
||||
const EntitiyCountTile({super.key, required this.count, required this.label, required this.icon});
|
||||
const EntityCountTile({super.key, required this.count, required this.label, required this.icon});
|
||||
|
||||
String zeroPadding(int number, int targetWidth) {
|
||||
final numStr = number.toString();
|
||||
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
|
||||
}
|
||||
|
||||
int calculateMaxDigits(double availableWidth) {
|
||||
const double charWidth = 11.0;
|
||||
return (availableWidth / charWidth).floor().clamp(1, 8);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final availableWidth = (screenWidth - 32 - 8) / 2;
|
||||
const double charWidth = 11.0;
|
||||
final maxDigits = ((availableWidth - 32) / charWidth).floor().clamp(1, 8);
|
||||
|
||||
return Container(
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
@@ -29,7 +30,6 @@ class EntitiyCountTile extends StatelessWidget {
|
||||
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Icon and Label
|
||||
@@ -38,33 +38,30 @@ class EntitiyCountTile extends StatelessWidget {
|
||||
children: [
|
||||
Icon(icon, color: context.primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Number
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxDigits = calculateMaxDigits(constraints.maxWidth);
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: zeroPadding(count, maxDigits),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||
),
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: TextStyle(color: context.primaryColor),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: zeroPadding(count, maxDigits),
|
||||
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
|
||||
),
|
||||
);
|
||||
},
|
||||
TextSpan(
|
||||
text: count.toString(),
|
||||
style: TextStyle(color: context.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -282,76 +282,87 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
_SectionHeaderText(text: "assets".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAssetCount,
|
||||
icon: Icons.smartphone,
|
||||
// 1. Wrap in IntrinsicHeight
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// 2. Stretch children vertically to fill the IntrinsicHeight
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAssetCount,
|
||||
icon: Icons.smartphone,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAssetCount,
|
||||
icon: Icons.cloud,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAssetCount,
|
||||
icon: Icons.cloud,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_SectionHeaderText(text: "albums".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAlbumCount,
|
||||
icon: Icons.smartphone,
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: localAlbumCount,
|
||||
icon: Icons.smartphone,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAlbumCount,
|
||||
icon: Icons.cloud,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "remote".t(context: context),
|
||||
count: remoteAlbumCount,
|
||||
icon: Icons.cloud,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_SectionHeaderText(text: "other".t(context: context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "memories".t(context: context),
|
||||
count: memoryCount,
|
||||
icon: Icons.calendar_today,
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "memories".t(context: context),
|
||||
count: memoryCount,
|
||||
icon: Icons.calendar_today,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: localHashedCount,
|
||||
icon: Icons.tag,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: localHashedCount,
|
||||
icon: Icons.tag,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// To be removed once the experimental feature is stable
|
||||
@@ -364,26 +375,29 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||
return counts.when(
|
||||
data: (c) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: c.total,
|
||||
icon: Icons.delete_outline,
|
||||
child: IntrinsicHeight(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "local".t(context: context),
|
||||
count: c.total,
|
||||
icon: Icons.delete_outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EntitiyCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: c.hashed,
|
||||
icon: Icons.tag,
|
||||
Expanded(
|
||||
child: EntityCountTile(
|
||||
label: "hashed_assets".t(context: context),
|
||||
count: c.hashed,
|
||||
icon: Icons.tag,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
|
||||
3
mobile/packages/ui/lib/immich_ui.dart
Normal file
3
mobile/packages/ui/lib/immich_ui.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
export 'src/buttons/close_button.dart';
|
||||
export 'src/buttons/icon_button.dart';
|
||||
export 'src/types.dart';
|
||||
25
mobile/packages/ui/lib/src/buttons/close_button.dart
Normal file
25
mobile/packages/ui/lib/src/buttons/close_button.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/buttons/icon_button.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichCloseButton extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichCloseButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.ghost,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImmichIconButton(
|
||||
key: key,
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onTap: onTap ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
48
mobile/packages/ui/lib/src/buttons/icon_button.dart
Normal file
48
mobile/packages/ui/lib/src/buttons/icon_button.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.filled,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final background = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.onPrimary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: onTap,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
9
mobile/packages/ui/lib/src/types.dart
Normal file
9
mobile/packages/ui/lib/src/types.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
enum ImmichVariant {
|
||||
filled,
|
||||
ghost,
|
||||
}
|
||||
|
||||
enum ImmichColor {
|
||||
primary,
|
||||
secondary,
|
||||
}
|
||||
55
mobile/packages/ui/pubspec.lock
Normal file
55
mobile/packages/ui/pubspec.lock
Normal file
@@ -0,0 +1,55 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
12
mobile/packages/ui/pubspec.yaml
Normal file
12
mobile/packages/ui/pubspec.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
name: immich_ui
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
@@ -1015,6 +1015,13 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.0"
|
||||
immich_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
||||
@@ -43,6 +43,8 @@ dependencies:
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.5.0
|
||||
image_picker: ^1.2.0
|
||||
immich_ui:
|
||||
path: './packages/ui'
|
||||
intl: ^0.20.2
|
||||
isar:
|
||||
git:
|
||||
|
||||
@@ -77,15 +77,15 @@ void main() {
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_rounded), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), findsNothing);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Bordered icon if likedId == null', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
|
||||
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.favorite_rounded), findsNothing);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Adds new like', (tester) async {
|
||||
|
||||
@@ -91,17 +91,17 @@ void main() {
|
||||
group('Like Activity', () {
|
||||
final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin);
|
||||
|
||||
testWidgets('Like contains filled heart as leading', (tester) async {
|
||||
testWidgets('Like contains filled thumbs-up as leading', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
// Leading widget should not be null
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.leading, isNotNull);
|
||||
|
||||
// And should have a favorite icon
|
||||
final favoIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.favorite_rounded);
|
||||
// And should have a thumb_up icon
|
||||
final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up);
|
||||
|
||||
expect(favoIconFinder, findsOneWidget);
|
||||
expect(thumbUpIconFinder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Like title is center aligned', (tester) async {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/node": "^24.10.3",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
214
plugins/package-lock.json
generated
214
plugins/package-lock.json
generated
@@ -15,9 +15,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
|
||||
"integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
|
||||
"integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz",
|
||||
"integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
|
||||
"integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -49,9 +49,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -66,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -83,9 +83,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -117,9 +117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -134,9 +134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -151,9 +151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz",
|
||||
"integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
|
||||
"integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -168,9 +168,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz",
|
||||
"integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
|
||||
"integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -202,9 +202,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz",
|
||||
"integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
|
||||
"integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -219,9 +219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz",
|
||||
"integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
|
||||
"integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -236,9 +236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz",
|
||||
"integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
|
||||
"integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -253,9 +253,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz",
|
||||
"integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
|
||||
"integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -270,9 +270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz",
|
||||
"integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
|
||||
"integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -287,9 +287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -321,9 +321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -338,9 +338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -355,9 +355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -372,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -389,9 +389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -406,9 +406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz",
|
||||
"integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
|
||||
"integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -423,9 +423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz",
|
||||
"integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
|
||||
"integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -440,9 +440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz",
|
||||
"integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
|
||||
"integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -467,9 +467,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
|
||||
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
|
||||
"integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -480,32 +480,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.0",
|
||||
"@esbuild/android-arm": "0.27.0",
|
||||
"@esbuild/android-arm64": "0.27.0",
|
||||
"@esbuild/android-x64": "0.27.0",
|
||||
"@esbuild/darwin-arm64": "0.27.0",
|
||||
"@esbuild/darwin-x64": "0.27.0",
|
||||
"@esbuild/freebsd-arm64": "0.27.0",
|
||||
"@esbuild/freebsd-x64": "0.27.0",
|
||||
"@esbuild/linux-arm": "0.27.0",
|
||||
"@esbuild/linux-arm64": "0.27.0",
|
||||
"@esbuild/linux-ia32": "0.27.0",
|
||||
"@esbuild/linux-loong64": "0.27.0",
|
||||
"@esbuild/linux-mips64el": "0.27.0",
|
||||
"@esbuild/linux-ppc64": "0.27.0",
|
||||
"@esbuild/linux-riscv64": "0.27.0",
|
||||
"@esbuild/linux-s390x": "0.27.0",
|
||||
"@esbuild/linux-x64": "0.27.0",
|
||||
"@esbuild/netbsd-arm64": "0.27.0",
|
||||
"@esbuild/netbsd-x64": "0.27.0",
|
||||
"@esbuild/openbsd-arm64": "0.27.0",
|
||||
"@esbuild/openbsd-x64": "0.27.0",
|
||||
"@esbuild/openharmony-arm64": "0.27.0",
|
||||
"@esbuild/sunos-x64": "0.27.0",
|
||||
"@esbuild/win32-arm64": "0.27.0",
|
||||
"@esbuild/win32-ia32": "0.27.0",
|
||||
"@esbuild/win32-x64": "0.27.0"
|
||||
"@esbuild/aix-ppc64": "0.27.1",
|
||||
"@esbuild/android-arm": "0.27.1",
|
||||
"@esbuild/android-arm64": "0.27.1",
|
||||
"@esbuild/android-x64": "0.27.1",
|
||||
"@esbuild/darwin-arm64": "0.27.1",
|
||||
"@esbuild/darwin-x64": "0.27.1",
|
||||
"@esbuild/freebsd-arm64": "0.27.1",
|
||||
"@esbuild/freebsd-x64": "0.27.1",
|
||||
"@esbuild/linux-arm": "0.27.1",
|
||||
"@esbuild/linux-arm64": "0.27.1",
|
||||
"@esbuild/linux-ia32": "0.27.1",
|
||||
"@esbuild/linux-loong64": "0.27.1",
|
||||
"@esbuild/linux-mips64el": "0.27.1",
|
||||
"@esbuild/linux-ppc64": "0.27.1",
|
||||
"@esbuild/linux-riscv64": "0.27.1",
|
||||
"@esbuild/linux-s390x": "0.27.1",
|
||||
"@esbuild/linux-x64": "0.27.1",
|
||||
"@esbuild/netbsd-arm64": "0.27.1",
|
||||
"@esbuild/netbsd-x64": "0.27.1",
|
||||
"@esbuild/openbsd-arm64": "0.27.1",
|
||||
"@esbuild/openbsd-x64": "0.27.1",
|
||||
"@esbuild/openharmony-arm64": "0.27.1",
|
||||
"@esbuild/sunos-x64": "0.27.1",
|
||||
"@esbuild/win32-arm64": "0.27.1",
|
||||
"@esbuild/win32-ia32": "0.27.1",
|
||||
"@esbuild/win32-x64": "0.27.1"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
|
||||
4144
pnpm-lock.yaml
generated
4144
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
|
||||
|
||||
FROM builder AS plugins
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./plugins/mise.toml ./plugins/
|
||||
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
||||
ENV MISE_DATA_DIR=/buildcache/mise
|
||||
RUN --mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
||||
RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise install --cd plugins
|
||||
|
||||
COPY ./plugins ./plugins/
|
||||
@@ -66,7 +68,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
--mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
cd plugins && mise run build
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
"compression": "^1.8.0",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cron": "4.3.3",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"cron": "4.3.5",
|
||||
"exiftool-vendored": "^34.0.0",
|
||||
"express": "^5.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@@ -134,7 +134,7 @@
|
||||
"@types/luxon": "^3.6.2",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/node": "^24.10.3",
|
||||
"@types/nodemailer": "^7.0.0",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -58,7 +58,7 @@ export class EnvDto {
|
||||
IMMICH_MICROSERVICES_METRICS_PORT?: number;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
IMMICH_PLUGINS_ENABLED?: boolean;
|
||||
IMMICH_ALLOW_EXTERNAL_PLUGINS?: boolean;
|
||||
|
||||
@Optional()
|
||||
@Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' })
|
||||
@@ -113,6 +113,9 @@ export class EnvDto {
|
||||
@Optional()
|
||||
IMMICH_THIRD_PARTY_SUPPORT_URL?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
IMMICH_ALLOW_SETUP?: boolean;
|
||||
|
||||
@IsIPRange({ requireCIDR: false }, { each: true })
|
||||
@Transform(({ value }) =>
|
||||
value && typeof value === 'string'
|
||||
|
||||
@@ -8,6 +8,8 @@ const getEnv = () => {
|
||||
|
||||
const resetEnv = () => {
|
||||
for (const env of [
|
||||
'IMMICH_ALLOW_EXTERNAL_PLUGINS',
|
||||
'IMMICH_ALLOW_SETUP',
|
||||
'IMMICH_ENV',
|
||||
'IMMICH_WORKERS_INCLUDE',
|
||||
'IMMICH_WORKERS_EXCLUDE',
|
||||
@@ -75,6 +77,9 @@ describe('getEnv', () => {
|
||||
configFile: undefined,
|
||||
logLevel: undefined,
|
||||
});
|
||||
|
||||
expect(config.plugins.external).toEqual({ allow: false });
|
||||
expect(config.setup).toEqual({ allow: true });
|
||||
});
|
||||
|
||||
describe('IMMICH_MEDIA_LOCATION', () => {
|
||||
@@ -84,6 +89,32 @@ describe('getEnv', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('IMMICH_ALLOW_EXTERNAL_PLUGINS', () => {
|
||||
it('should disable plugins', () => {
|
||||
process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'false';
|
||||
const config = getEnv();
|
||||
expect(config.plugins.external).toEqual({ allow: false });
|
||||
});
|
||||
|
||||
it('should throw an error for invalid value', () => {
|
||||
process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'invalid';
|
||||
expect(() => getEnv()).toThrowError('IMMICH_ALLOW_EXTERNAL_PLUGINS must be a boolean value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('IMMICH_ALLOW_SETUP', () => {
|
||||
it('should disable setup', () => {
|
||||
process.env.IMMICH_ALLOW_SETUP = 'false';
|
||||
const { setup } = getEnv();
|
||||
expect(setup).toEqual({ allow: false });
|
||||
});
|
||||
|
||||
it('should throw an error for invalid value', () => {
|
||||
process.env.IMMICH_ALLOW_SETUP = 'invalid';
|
||||
expect(() => getEnv()).toThrowError('IMMICH_ALLOW_SETUP must be a boolean value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('database', () => {
|
||||
it('should use defaults', () => {
|
||||
const { database } = getEnv();
|
||||
|
||||
@@ -90,6 +90,10 @@ export interface EnvData {
|
||||
|
||||
redis: RedisOptions;
|
||||
|
||||
setup: {
|
||||
allow: boolean;
|
||||
};
|
||||
|
||||
telemetry: {
|
||||
apiPort: number;
|
||||
microservicesPort: number;
|
||||
@@ -104,8 +108,10 @@ export interface EnvData {
|
||||
workers: ImmichWorker[];
|
||||
|
||||
plugins: {
|
||||
enabled: boolean;
|
||||
installFolder?: string;
|
||||
external: {
|
||||
allow: boolean;
|
||||
installFolder?: string;
|
||||
};
|
||||
};
|
||||
|
||||
noColor: boolean;
|
||||
@@ -313,6 +319,10 @@ const getEnv = (): EnvData => {
|
||||
corePlugin: join(buildFolder, 'corePlugin'),
|
||||
},
|
||||
|
||||
setup: {
|
||||
allow: dto.IMMICH_ALLOW_SETUP ?? true,
|
||||
},
|
||||
|
||||
storage: {
|
||||
ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS,
|
||||
mediaLocation: dto.IMMICH_MEDIA_LOCATION,
|
||||
@@ -327,8 +337,10 @@ const getEnv = (): EnvData => {
|
||||
workers,
|
||||
|
||||
plugins: {
|
||||
enabled: !!dto.IMMICH_PLUGINS_ENABLED,
|
||||
installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER,
|
||||
external: {
|
||||
allow: dto.IMMICH_ALLOW_EXTERNAL_PLUGINS ?? false,
|
||||
installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER,
|
||||
},
|
||||
},
|
||||
|
||||
noColor: !!dto.NO_COLOR,
|
||||
|
||||
@@ -89,6 +89,7 @@ export class MetadataRepository {
|
||||
// Enable exiftool LFS to parse metadata for files larger than 2GB.
|
||||
readArgs: ['-api', 'largefilesupport=1'],
|
||||
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
|
||||
taskTimeoutMillis: 2 * 60 * 1000,
|
||||
});
|
||||
|
||||
constructor(private logger: LoggingRepository) {
|
||||
|
||||
@@ -165,6 +165,11 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
|
||||
const { setup } = this.configRepository.getEnv();
|
||||
if (!setup.allow) {
|
||||
throw new BadRequestException('Admin setup is disabled');
|
||||
}
|
||||
|
||||
const adminUser = await this.userRepository.getAdmin();
|
||||
if (adminUser) {
|
||||
throw new BadRequestException('The server already has an admin');
|
||||
|
||||
@@ -1034,7 +1034,10 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
|
||||
it('should use Duration from exif', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
originalPath: '/original/path.webp',
|
||||
});
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
@@ -1046,6 +1049,7 @@ describe(MetadataService.name, () => {
|
||||
it('should prefer Duration from exif over sidecar', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
originalPath: '/original/path.webp',
|
||||
files: [
|
||||
{
|
||||
id: 'some-id',
|
||||
@@ -1063,6 +1067,16 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
});
|
||||
|
||||
it('should ignore all Duration tags for definitely static images', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.imageDng);
|
||||
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.imageDng.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null }));
|
||||
});
|
||||
|
||||
it('should ignore Duration from exif for videos', async () => {
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||
mockReadTags({ Duration: 123 }, {});
|
||||
|
||||
@@ -32,6 +32,7 @@ import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { isAssetChecksumConstraint } from 'src/utils/database';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { isFaceImportEnabled } from 'src/utils/misc';
|
||||
import { upsertTags } from 'src/utils/tag';
|
||||
|
||||
@@ -486,7 +487,8 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
// prefer duration from video tags
|
||||
if (videoTags) {
|
||||
// don't save duration if asset is definitely not an animated image (see e.g. CR3 with Duration: 1s)
|
||||
if (videoTags || !mimeTypes.isPossiblyAnimatedImage(asset.originalPath)) {
|
||||
delete mediaTags.Duration;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ export class PluginService extends BaseService {
|
||||
this.logger.log(`Successfully processed core plugin: ${coreManifest.name} (version ${coreManifest.version})`);
|
||||
|
||||
// Load external plugins
|
||||
if (plugins.enabled && plugins.installFolder) {
|
||||
await this.loadExternalPlugins(plugins.installFolder);
|
||||
if (plugins.external.allow && plugins.external.installFolder) {
|
||||
await this.loadExternalPlugins(plugins.external.installFolder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user