mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 09:15:34 +03:00
Compare commits
172 Commits
7447d407eb
...
feat/datab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21e2e9415c | ||
|
|
4c71336e95 | ||
|
|
5ddc509cc4 | ||
|
|
2ceeb589de | ||
|
|
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 | ||
|
|
75a7c9c06c | ||
|
|
ae8f5a6673 | ||
|
|
31f2c7b505 | ||
|
|
e958516318 | ||
|
|
0d05c0d4ae | ||
|
|
4e2187acf9 | ||
|
|
adc2d5d1e5 | ||
|
|
6b9cc855a5 | ||
|
|
02265ba224 | ||
|
|
cf3686a509 | ||
|
|
3019091733 | ||
|
|
4296211c61 | ||
|
|
207a8bc55a | ||
|
|
a63b418507 | ||
|
|
fe8eb85e37 | ||
|
|
4659ceb425 | ||
|
|
17dfcedad6 | ||
|
|
20d1e610ce | ||
|
|
305bf60f97 | ||
|
|
f9d2a9707d | ||
|
|
ef944c29d3 | ||
|
|
274775d876 | ||
|
|
0945e18564 | ||
|
|
e0428b565a | ||
|
|
9b955508e9 | ||
|
|
a79b4bdc47 | ||
|
|
94af1bba4d | ||
|
|
b5ff460a55 | ||
|
|
8b1ba11e0b | ||
|
|
a7fd19db52 | ||
|
|
db7169ea01 | ||
|
|
cede65f2dd | ||
|
|
e355dccc48 | ||
|
|
8dd865d054 | ||
|
|
e3f350ea60 | ||
|
|
6ec10a5f15 | ||
|
|
9cb968116b | ||
|
|
52edcdee60 | ||
|
|
96426fec7e | ||
|
|
47f5232a5f | ||
|
|
a091ca76e7 | ||
|
|
c8fea45731 | ||
|
|
390f0b2817 | ||
|
|
1cdffeb3be | ||
|
|
87f34ba505 | ||
|
|
ca116caafb | ||
|
|
86b7b1c44d | ||
|
|
95d9bcb3f1 | ||
|
|
0f145a5b52 | ||
|
|
481ec02edb | ||
|
|
9f5f90b2ff | ||
|
|
1ad2282166 | ||
|
|
b99d92961c | ||
|
|
45b5752cbf | ||
|
|
220d63e035 | ||
|
|
3be039b953 | ||
|
|
e2ca0c6f67 | ||
|
|
f84bdc14d5 | ||
|
|
fd6f043aa4 | ||
|
|
5a6083f53c | ||
|
|
a61f9d7a26 | ||
|
|
3863ff73ef | ||
|
|
534a9f50b6 | ||
|
|
b46d6cda65 | ||
|
|
86d8e1a092 | ||
|
|
0940c313ac | ||
|
|
f6316ca0c8 | ||
|
|
539167eb88 | ||
|
|
5bca8808a1 | ||
|
|
e93652a4a5 | ||
|
|
ac9a587063 | ||
|
|
f7b59f50ed | ||
|
|
53ef26a5e4 | ||
|
|
6cefb9ca95 | ||
|
|
fdacf0ec57 | ||
|
|
cbf3a2c3cb | ||
|
|
d2a4dd67d8 | ||
|
|
874782edf0 | ||
|
|
a7245627fc | ||
|
|
174670a1b7 | ||
|
|
a3c6d71a58 | ||
|
|
19ba23056c | ||
|
|
3d2d7fa64c | ||
|
|
fccb31d1d8 | ||
|
|
8405a9bf0c | ||
|
|
3933b23e2c | ||
|
|
824f6e5b05 | ||
|
|
270d7e3cdc | ||
|
|
8463968712 | ||
|
|
5be08274ff | ||
|
|
161918e9ca | ||
|
|
d6e3d26cfc | ||
|
|
d5351de26f | ||
|
|
ed4a850a01 | ||
|
|
9d4ad11cff | ||
|
|
b887d4f557 | ||
|
|
2e15012257 | ||
|
|
56a4159295 | ||
|
|
f69c49a60f | ||
|
|
f778a4260b | ||
|
|
31f4665d35 | ||
|
|
53a74a7279 | ||
|
|
dd1cf12aaa | ||
|
|
31410c3c20 | ||
|
|
26587dd690 | ||
|
|
442fe6e3d0 | ||
|
|
af741a4761 | ||
|
|
7c2e8b1d62 | ||
|
|
56c93a71c0 | ||
|
|
c090a1a9d9 | ||
|
|
d040de2d52 | ||
|
|
73ae766d9f | ||
|
|
edc1333db1 | ||
|
|
b01b63b25a | ||
|
|
7e7d6af66b | ||
|
|
0ae03f68cf | ||
|
|
0419539c08 | ||
|
|
f67153e44b | ||
|
|
cc7895244d | ||
|
|
a6fb942fca | ||
|
|
9cea3d7b2f |
2
.github/package.json
vendored
2
.github/package.json
vendored
@@ -4,6 +4,6 @@
|
|||||||
"format:fix": "prettier --write ."
|
"format:fix": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.5.3"
|
"prettier": "^3.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
.github/workflows/build-mobile.yml
vendored
10
.github/workflows/build-mobile.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
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:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -222,6 +222,7 @@ jobs:
|
|||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
|
bundler-cache: true
|
||||||
working-directory: ./mobile/ios
|
working-directory: ./mobile/ios
|
||||||
|
|
||||||
- name: Install CocoaPods dependencies
|
- name: Install CocoaPods dependencies
|
||||||
@@ -229,13 +230,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pod install
|
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
|
- name: Create API Key
|
||||||
env:
|
env:
|
||||||
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
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
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|||||||
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.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
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
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './docs/.nvmrc'
|
node-version-file: './docs/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
4
.github/workflows/fix-format.yml
vendored
4
.github/workflows/fix-format.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-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:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
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
|
- name: Generate a token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
if: ${{ inputs.skip != true }}
|
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:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
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:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-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:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-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:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -144,7 +144,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ env.IMMICH_VERSION }}
|
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:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-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:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -159,7 +159,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create PR
|
- name: Create PR
|
||||||
id: 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:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'
|
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:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-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:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.version.outputs.result }}
|
tag_name: ${{ steps.version.outputs.result }}
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
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
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|||||||
26
.github/workflows/test.yml
vendored
26
.github/workflows/test.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -210,7 +210,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -254,7 +254,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -292,7 +292,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -340,7 +340,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -387,7 +387,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -426,7 +426,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -481,7 +481,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -617,7 +617,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './.github/.nvmrc'
|
node-version-file: './.github/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -668,7 +668,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -730,7 +730,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,7 +7,7 @@
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
docker/upload
|
docker/upload
|
||||||
docker/library
|
docker/library*
|
||||||
uploads
|
uploads
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.3",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^62.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ services:
|
|||||||
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
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_DOCUMENTATION_URL: https://docs.immich.app
|
||||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
|
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
|
||||||
ulimits:
|
|
||||||
nofile:
|
|
||||||
soft: 1048576
|
|
||||||
hard: 1048576
|
|
||||||
ports:
|
ports:
|
||||||
- 9230:9230
|
- 9230:9230
|
||||||
- 9231:9231
|
- 9231:9231
|
||||||
@@ -100,10 +96,6 @@ services:
|
|||||||
- app-node_modules:/usr/src/app/node_modules
|
- app-node_modules:/usr/src/app/node_modules
|
||||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||||
- coverage:/usr/src/app/web/coverage
|
- coverage:/usr/src/app/web/coverage
|
||||||
ulimits:
|
|
||||||
nofile:
|
|
||||||
soft: 1048576
|
|
||||||
hard: 1048576
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
immich-server:
|
immich-server:
|
||||||
@@ -135,7 +127,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
@@ -83,7 +83,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:49214755b6153f90a597adcbff0252cc61069f8ab69ce8411285cd4a560e8038
|
image: prom/prometheus@sha256:d936808bdea528155c0154a922cd42fd75716b8bb7ba302641350f9f3eaeba09
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27
|
image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
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)
|
# disable buffering uploads to prevent OOM on reverse proxy server and make uploads twice as fast (no pause)
|
||||||
proxy_request_buffering off;
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
# increase body buffer to avoid limiting upload speed
|
||||||
|
client_body_buffer_size 1024k;
|
||||||
|
|
||||||
# Set headers
|
# Set headers
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -32,8 +35,6 @@ server {
|
|||||||
|
|
||||||
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
# enable websockets: http://nginx.org/en/docs/http/websocket.html
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
|
||||||
# set timeout
|
# set timeout
|
||||||
@@ -43,6 +44,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://<backend_url>:2283;
|
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
|
# useful when using Let's Encrypt http-01 challenge
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Password login has been enabled.
|
|||||||
Disable Maintenance Mode
|
Disable Maintenance Mode
|
||||||
|
|
||||||
```
|
```
|
||||||
immich-admin disable-maintenace-mode
|
immich-admin disable-maintenance-mode
|
||||||
Maintenance mode has been disabled.
|
Maintenance mode has been disabled.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"@docusaurus/module-type-aliases": "~3.9.0",
|
"@docusaurus/module-type-aliases": "~3.9.0",
|
||||||
"@docusaurus/tsconfig": "^3.7.0",
|
"@docusaurus/tsconfig": "^3.7.0",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.3",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^62.0.0",
|
"eslint-plugin-unicorn": "^62.0.0",
|
||||||
"exiftool-vendored": "^33.0.0",
|
"exiftool-vendored": "^34.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"oidc-provider": "^9.0.0",
|
"oidc-provider": "^9.0.0",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
|
|||||||
267
e2e/src/api/specs/database-backups.e2e-spec.ts
Normal file
267
e2e/src/api/specs/database-backups.e2e-spec.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import { LoginResponseDto, ManualJobName } from '@immich/sdk';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('/admin/database-backups', () => {
|
||||||
|
let cookie: string | undefined;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /', async () => {
|
||||||
|
it('should succeed and be empty', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/admin/database-backups')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
backups: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a created backup', async () => {
|
||||||
|
await utils.createJob(admin.accessToken, {
|
||||||
|
name: ManualJobName.BackupDatabase,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'backupDatabase');
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/admin/database-backups')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
backups: [expect.stringMatching(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /', async () => {
|
||||||
|
it('should delete backup', async () => {
|
||||||
|
const filename = await utils.createBackup(admin.accessToken);
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.delete(`/admin/database-backups`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ backups: [filename] });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const { status: listStatus, body } = await request(app)
|
||||||
|
.get('/admin/database-backups')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(listStatus).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
backups: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// => action: restore database flow
|
||||||
|
|
||||||
|
describe.sequential('POST /start-restore', () => {
|
||||||
|
afterAll(async () => {
|
||||||
|
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ action: 'end' });
|
||||||
|
await utils.poll(
|
||||||
|
() => request(app).get('/server/config'),
|
||||||
|
({ status, body }) => status === 200 && !body.maintenanceMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('should not work when the server is configured', async () => {
|
||||||
|
const { status, body } = await request(app).post('/admin/database-backups/start-restore').send();
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest('The server already has an admin'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('should enter maintenance mode in "database restore mode"', async () => {
|
||||||
|
await utils.resetDatabase(); // reset database before running this test
|
||||||
|
|
||||||
|
const { status, headers } = await request(app).post('/admin/database-backups/start-restore').send();
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// => action: restore database
|
||||||
|
|
||||||
|
describe.sequential('POST /backups/restore', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.disconnectDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await utils.connectDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('should restore a backup', { timeout: 60_000 }, async () => {
|
||||||
|
const filename = await utils.createBackup(admin.accessToken);
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.post('/admin/maintenance')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
action: 'restore_database',
|
||||||
|
restoreBackupFilename: filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 60_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.sequential('fail to restore a corrupted backup', { timeout: 60_000 }, async () => {
|
||||||
|
await utils.prepareTestBackup('corrupted');
|
||||||
|
|
||||||
|
const { status, headers } = await request(app)
|
||||||
|
.post('/admin/maintenance')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
action: 'restore_database',
|
||||||
|
restoreBackupFilename: 'development-corrupted.sql.gz',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body.maintenanceMode;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interval: 500,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
error: 'Something went wrong, see logs!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { status: status2, body: body2 } = await request(app)
|
||||||
|
.get('/admin/maintenance/status')
|
||||||
|
.set('cookie', cookie!)
|
||||||
|
.send({ token: 'token' });
|
||||||
|
expect(status2).toBe(200);
|
||||||
|
expect(body2).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
active: true,
|
||||||
|
action: 'restore_database',
|
||||||
|
error: expect.stringContaining('IM CORRUPTED'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
|
||||||
|
action: 'end',
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.poll(
|
||||||
|
() => request(app).get('/server/config'),
|
||||||
|
({ status, body }) => status === 200 && !body.maintenanceMode,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ describe('/admin/maintenance', () => {
|
|||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
// => outside of maintenance mode
|
// => outside of maintenance mode
|
||||||
@@ -26,6 +27,17 @@ describe('/admin/maintenance', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /status', async () => {
|
||||||
|
it('to always indicate we are not in maintenance mode', async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
active: false,
|
||||||
|
action: 'end',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /login', async () => {
|
describe('POST /login', async () => {
|
||||||
it('should not work out of maintenance mode', async () => {
|
it('should not work out of maintenance mode', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
|
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
|
||||||
@@ -39,6 +51,7 @@ describe('/admin/maintenance', () => {
|
|||||||
describe.sequential('POST /', () => {
|
describe.sequential('POST /', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance').send({
|
const { status, body } = await request(app).post('/admin/maintenance').send({
|
||||||
|
active: false,
|
||||||
action: 'end',
|
action: 'end',
|
||||||
});
|
});
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
@@ -69,6 +82,7 @@ describe('/admin/maintenance', () => {
|
|||||||
.send({
|
.send({
|
||||||
action: 'start',
|
action: 'start',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
|
|
||||||
cookie = headers['set-cookie'][0].split(';')[0];
|
cookie = headers['set-cookie'][0].split(';')[0];
|
||||||
@@ -79,12 +93,13 @@ describe('/admin/maintenance', () => {
|
|||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const { body } = await request(app).get('/server/config');
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
return body.maintenanceMode;
|
return body.maintenanceMode;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
interval: 5e2,
|
interval: 500,
|
||||||
timeout: 1e4,
|
timeout: 10_000,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.toBeTruthy();
|
.toBeTruthy();
|
||||||
@@ -102,6 +117,17 @@ describe('/admin/maintenance', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /status', async () => {
|
||||||
|
it('to indicate we are in maintenance mode', async () => {
|
||||||
|
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
active: true,
|
||||||
|
action: 'start',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /login', async () => {
|
describe('POST /login', async () => {
|
||||||
it('should fail without cookie or token in body', async () => {
|
it('should fail without cookie or token in body', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
|
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
|
||||||
@@ -158,12 +184,13 @@ describe('/admin/maintenance', () => {
|
|||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const { body } = await request(app).get('/server/config');
|
const { status, body } = await request(app).get('/server/config');
|
||||||
|
expect(status).toBe(200);
|
||||||
return body.maintenanceMode;
|
return body.maintenanceMode;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
interval: 5e2,
|
interval: 500,
|
||||||
timeout: 1e4,
|
timeout: 10_000,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.toBeFalsy();
|
.toBeFalsy();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
CheckExistingAssetsDto,
|
CheckExistingAssetsDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
|
JobCreateDto,
|
||||||
MaintenanceAction,
|
MaintenanceAction,
|
||||||
|
ManualJobName,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
Permission,
|
Permission,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -21,6 +23,7 @@ import {
|
|||||||
checkExistingAssets,
|
checkExistingAssets,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
|
createJob,
|
||||||
createLibrary,
|
createLibrary,
|
||||||
createPartner,
|
createPartner,
|
||||||
createPerson,
|
createPerson,
|
||||||
@@ -28,10 +31,12 @@ import {
|
|||||||
createStack,
|
createStack,
|
||||||
createUserAdmin,
|
createUserAdmin,
|
||||||
deleteAssets,
|
deleteAssets,
|
||||||
|
deleteDatabaseBackup,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getConfig,
|
getConfig,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
getQueuesLegacy,
|
getQueuesLegacy,
|
||||||
|
listDatabaseBackups,
|
||||||
login,
|
login,
|
||||||
runQueueCommandLegacy,
|
runQueueCommandLegacy,
|
||||||
scanLibrary,
|
scanLibrary,
|
||||||
@@ -52,11 +57,15 @@ import {
|
|||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
import { exec, spawn } from 'node:child_process';
|
import { exec, spawn } from 'node:child_process';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { mkdtemp } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
import { createGzip } from 'node:zlib';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
@@ -84,8 +93,9 @@ export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer $
|
|||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
export const immichCli = (args: string[]) =>
|
export const immichCli = (args: string[]) =>
|
||||||
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
|
||||||
export const immichAdmin = (args: string[]) =>
|
export const dockerExec = (args: string[]) =>
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
|
||||||
|
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
|
||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
@@ -149,13 +159,27 @@ const onEvent = ({ event, id }: { event: EventType; id: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
resetDatabase: async (tables?: string[]) => {
|
connectDatabase: async () => {
|
||||||
try {
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = new pg.Client(dbUrl);
|
client = new pg.Client(dbUrl);
|
||||||
|
client.on('end', () => (client = null));
|
||||||
|
client.on('error', () => (client = null));
|
||||||
await client.connect();
|
await client.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnectDatabase: async () => {
|
||||||
|
if (client) {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetDatabase: async (tables?: string[]) => {
|
||||||
|
try {
|
||||||
|
client = await utils.connectDatabase();
|
||||||
|
|
||||||
tables = tables || [
|
tables = tables || [
|
||||||
// TODO e2e test for deleting a stack, since it is quite complex
|
// TODO e2e test for deleting a stack, since it is quite complex
|
||||||
'stack',
|
'stack',
|
||||||
@@ -481,6 +505,9 @@ export const utils = {
|
|||||||
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
||||||
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
|
createJob: async (accessToken: string, jobCreateDto: JobCreateDto) =>
|
||||||
|
createJob({ jobCreateDto }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
||||||
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
@@ -559,6 +586,36 @@ export const utils = {
|
|||||||
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
|
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createBackup: async (accessToken: string) => {
|
||||||
|
await utils.createJob(accessToken, {
|
||||||
|
name: ManualJobName.BackupDatabase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await utils.poll(
|
||||||
|
() => request(app).get('/admin/database-backups').set('Authorization', `Bearer ${accessToken}`),
|
||||||
|
({ status, body }) => status === 200 && body.backups.length === 1,
|
||||||
|
({ body }) => body.backups[0],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetBackups: async (accessToken: string) => {
|
||||||
|
const { backups } = await listDatabaseBackups({ headers: asBearerAuth(accessToken) });
|
||||||
|
await deleteDatabaseBackup({ databaseBackupDeleteDto: { backups } }, { headers: asBearerAuth(accessToken) });
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareTestBackup: async (generate: 'corrupted') => {
|
||||||
|
const dir = await mkdtemp(join(tmpdir(), 'test-'));
|
||||||
|
const fn = join(dir, 'file');
|
||||||
|
|
||||||
|
const sql = Readable.from('IM CORRUPTED;');
|
||||||
|
const gzip = createGzip();
|
||||||
|
const writeStream = createWriteStream(fn);
|
||||||
|
await pipeline(sql, gzip, writeStream);
|
||||||
|
|
||||||
|
await executeCommand('docker', ['cp', fn, `immich-e2e-server:/data/backups/development-${generate}.sql.gz`])
|
||||||
|
.promise;
|
||||||
|
},
|
||||||
|
|
||||||
resetAdminConfig: async (accessToken: string) => {
|
resetAdminConfig: async (accessToken: string) => {
|
||||||
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
||||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||||
@@ -601,6 +658,25 @@ export const utils = {
|
|||||||
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
||||||
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async poll<T>(cb: () => Promise<T>, validate: (value: T) => boolean, map?: (value: T) => any) {
|
||||||
|
let timeout = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const data = await cb();
|
||||||
|
if (validate(data)) {
|
||||||
|
return map ? map(data) : data;
|
||||||
|
}
|
||||||
|
timeout++;
|
||||||
|
if (timeout >= 10) {
|
||||||
|
throw 'Could not clean up test.';
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5e2));
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.initSdk();
|
utils.initSdk();
|
||||||
|
|||||||
75
e2e/src/web/specs/database-backups.e2e-spec.ts
Normal file
75
e2e/src/web/specs/database-backups.e2e-spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { utils } from 'src/utils';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
|
test.describe('Database Backups', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
utils.initSdk();
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore a backup from settings', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
await utils.createBackup(admin.accessToken);
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
await page.goto('/admin/maintenance?isOpen=backups');
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.locator('#bits-c2').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await page.waitForURL('/admin/maintenance**', { timeout: 60_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle backup restore failure', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
await utils.prepareTestBackup('corrupted');
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
await page.goto('/admin/maintenance?isOpen=backups');
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.locator('#bits-c2').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await expect(page.getByText('IM CORRUPTED')).toBeVisible({ timeout: 60_000 });
|
||||||
|
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
||||||
|
await page.waitForURL('/admin/maintenance**');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore a backup from onboarding', async ({ context, page }) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
await utils.resetBackups(admin.accessToken);
|
||||||
|
await utils.createBackup(admin.accessToken);
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
await utils.resetDatabase();
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('button', { name: 'Restore from backup' }).click();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.waitForURL('/maintenance**');
|
||||||
|
} catch {
|
||||||
|
// when chained with the rest of the tests
|
||||||
|
// this navigation may fail..? not sure why...
|
||||||
|
await page.goto('/maintenance');
|
||||||
|
await page.waitForURL('/maintenance**');
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||||
|
await page.locator('#bits-c2').getByRole('button', { name: 'Restore' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/maintenance?**');
|
||||||
|
await page.waitForURL('/photos', { timeout: 60_000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,12 +16,12 @@ test.describe('Maintenance', () => {
|
|||||||
test('enter and exit maintenance mode', async ({ context, page }) => {
|
test('enter and exit maintenance mode', async ({ context, page }) => {
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
await page.goto('/admin/system-settings?isOpen=maintenance');
|
await page.goto('/admin/maintenance');
|
||||||
await page.getByRole('button', { name: 'Start maintenance mode' }).click();
|
await page.getByRole('button', { name: 'Switch to maintenance mode' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 });
|
||||||
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
await page.getByRole('button', { name: 'End maintenance mode' }).click();
|
||||||
await page.waitForURL('**/admin/system-settings*', { timeout: 10_000 });
|
await page.waitForURL('**/admin/maintenance*', { timeout: 10_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
||||||
|
|||||||
39
i18n/en.json
39
i18n/en.json
@@ -78,7 +78,6 @@
|
|||||||
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
|
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
|
||||||
"export_config_as_json_description": "Download the current system config as a JSON file",
|
"export_config_as_json_description": "Download the current system config as a JSON file",
|
||||||
"external_libraries_page_description": "Admin external library page",
|
"external_libraries_page_description": "Admin external library page",
|
||||||
"external_library_management": "External Library Management",
|
|
||||||
"face_detection": "Face detection",
|
"face_detection": "Face detection",
|
||||||
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
|
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
|
||||||
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
|
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
|
||||||
@@ -182,10 +181,19 @@
|
|||||||
"machine_learning_smart_search_enabled": "Enable smart search",
|
"machine_learning_smart_search_enabled": "Enable smart search",
|
||||||
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
||||||
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
||||||
|
"maintenance_delete_backup": "Delete Backup",
|
||||||
|
"maintenance_delete_backup_description": "This file will be irrevocably deleted.",
|
||||||
|
"maintenance_delete_error": "Failed to delete backup.",
|
||||||
|
"maintenance_restore_backup": "Restore Backup",
|
||||||
|
"maintenance_restore_backup_description": "Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.",
|
||||||
|
"maintenance_restore_database_backup": "Restore database backup",
|
||||||
|
"maintenance_restore_database_backup_description": "Rollback to an earlier database state using a backup file",
|
||||||
"maintenance_settings": "Maintenance",
|
"maintenance_settings": "Maintenance",
|
||||||
"maintenance_settings_description": "Put Immich into maintenance mode.",
|
"maintenance_settings_description": "Put Immich into maintenance mode.",
|
||||||
"maintenance_start": "Start maintenance mode",
|
"maintenance_start": "Switch to maintenance mode",
|
||||||
"maintenance_start_error": "Failed to start maintenance mode.",
|
"maintenance_start_error": "Failed to start maintenance mode.",
|
||||||
|
"maintenance_upload_backup": "Upload database backup file",
|
||||||
|
"maintenance_upload_backup_error": "Could not upload backup, is it an .sql/.sql.gz file?",
|
||||||
"manage_concurrency": "Manage Concurrency",
|
"manage_concurrency": "Manage Concurrency",
|
||||||
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
|
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
|
||||||
"manage_log_settings": "Manage log settings",
|
"manage_log_settings": "Manage log settings",
|
||||||
@@ -653,6 +661,7 @@
|
|||||||
"backup_options_page_title": "Backup options",
|
"backup_options_page_title": "Backup options",
|
||||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||||
"backup_settings_subtitle": "Manage upload settings",
|
"backup_settings_subtitle": "Manage upload settings",
|
||||||
|
"backup_upload_details_page_more_details": "Tap for more details",
|
||||||
"backward": "Backward",
|
"backward": "Backward",
|
||||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||||
@@ -719,6 +728,7 @@
|
|||||||
"check_corrupt_asset_backup_button": "Perform check",
|
"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_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",
|
"check_logs": "Check Logs",
|
||||||
|
"checksum": "Checksum",
|
||||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||||
"city": "City",
|
"city": "City",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -802,6 +812,12 @@
|
|||||||
"create_user": "Create user",
|
"create_user": "Create user",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"created_at": "Created",
|
"created_at": "Created",
|
||||||
|
"created_day_ago": "Created 1 day ago",
|
||||||
|
"created_days_ago": "Created {count} days ago",
|
||||||
|
"created_hour_ago": "Created 1 hour ago",
|
||||||
|
"created_hours_ago": "Created {count} hours ago",
|
||||||
|
"created_minute_ago": "Created 1 minute ago",
|
||||||
|
"created_minutes_ago": "Created {count} minutes ago",
|
||||||
"creating_linked_albums": "Creating linked albums...",
|
"creating_linked_albums": "Creating linked albums...",
|
||||||
"crop": "Crop",
|
"crop": "Crop",
|
||||||
"curated_object_page_title": "Things",
|
"curated_object_page_title": "Things",
|
||||||
@@ -1167,6 +1183,7 @@
|
|||||||
"header_settings_header_name_input": "Header name",
|
"header_settings_header_name_input": "Header name",
|
||||||
"header_settings_header_value_input": "Header value",
|
"header_settings_header_value_input": "Header value",
|
||||||
"headers_settings_tile_title": "Custom proxy headers",
|
"headers_settings_tile_title": "Custom proxy headers",
|
||||||
|
"height": "Height",
|
||||||
"hi_user": "Hi {name} ({email})",
|
"hi_user": "Hi {name} ({email})",
|
||||||
"hide_all_people": "Hide all people",
|
"hide_all_people": "Hide all people",
|
||||||
"hide_gallery": "Hide gallery",
|
"hide_gallery": "Hide gallery",
|
||||||
@@ -1289,6 +1306,7 @@
|
|||||||
"local": "Local",
|
"local": "Local",
|
||||||
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
||||||
"local_assets": "Local Assets",
|
"local_assets": "Local Assets",
|
||||||
|
"local_id": "Local ID",
|
||||||
"local_media_summary": "Local Media Summary",
|
"local_media_summary": "Local Media Summary",
|
||||||
"local_network": "Local network",
|
"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",
|
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
||||||
@@ -1340,10 +1358,26 @@
|
|||||||
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
|
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
|
||||||
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
|
"main_branch_warning": "You're using a development version; we strongly recommend using a release version!",
|
||||||
"main_menu": "Main menu",
|
"main_menu": "Main menu",
|
||||||
|
"maintenance_action_restore": "Restoring Database",
|
||||||
"maintenance_description": "Immich has been put into <link>maintenance mode</link>.",
|
"maintenance_description": "Immich has been put into <link>maintenance mode</link>.",
|
||||||
"maintenance_end": "End maintenance mode",
|
"maintenance_end": "End maintenance mode",
|
||||||
"maintenance_end_error": "Failed to end maintenance mode.",
|
"maintenance_end_error": "Failed to end maintenance mode.",
|
||||||
"maintenance_logged_in_as": "Currently logged in as {user}",
|
"maintenance_logged_in_as": "Currently logged in as {user}",
|
||||||
|
"maintenance_restore_from_backup": "Restore From Backup",
|
||||||
|
"maintenance_restore_library": "Restore Your Library",
|
||||||
|
"maintenance_restore_library_confirm": "If this looks correct, continue to restoring a backup!",
|
||||||
|
"maintenance_restore_library_description": "Restoring Database",
|
||||||
|
"maintenance_restore_library_folder_has_files": "{folder} has {count} folder(s)",
|
||||||
|
"maintenance_restore_library_folder_no_files": "{folder} is missing files!",
|
||||||
|
"maintenance_restore_library_folder_pass": "readable and writable",
|
||||||
|
"maintenance_restore_library_folder_read_fail": "not readable",
|
||||||
|
"maintenance_restore_library_folder_write_fail": "not writable",
|
||||||
|
"maintenance_restore_library_hint_missing_files": "You may be missing important files",
|
||||||
|
"maintenance_restore_library_hint_regenerate_later": "You can regenerate these later in settings",
|
||||||
|
"maintenance_restore_library_hint_storage_template_missing_files": "Using storage template? You may be missing files",
|
||||||
|
"maintenance_restore_library_loading": "Loading integrity checks and heuristics…",
|
||||||
|
"maintenance_task_backup": "Creating a backup of the existing database…",
|
||||||
|
"maintenance_task_restore": "Restoring the chosen backup…",
|
||||||
"maintenance_title": "Temporarily Unavailable",
|
"maintenance_title": "Temporarily Unavailable",
|
||||||
"make": "Make",
|
"make": "Make",
|
||||||
"manage_geolocation": "Manage location",
|
"manage_geolocation": "Manage location",
|
||||||
@@ -2219,6 +2253,7 @@
|
|||||||
"week": "Week",
|
"week": "Week",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"welcome_to_immich": "Welcome to Immich",
|
"welcome_to_immich": "Welcome to Immich",
|
||||||
|
"width": "Width",
|
||||||
"wifi_name": "Wi-Fi Name",
|
"wifi_name": "Wi-Fi Name",
|
||||||
"workflow": "Workflow",
|
"workflow": "Workflow",
|
||||||
"wrong_pin_code": "Wrong PIN code",
|
"wrong_pin_code": "Wrong PIN code",
|
||||||
|
|||||||
@@ -137,8 +137,7 @@
|
|||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>We need local network permission to connect to the local server using IP address and
|
<string>We need local network permission to connect to the local server using IP address and allow the casting feature to work</string>
|
||||||
allow the casting feature to work</string>
|
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||||
<key>NSLocationUsageDescription</key>
|
<key>NSLocationUsageDescription</key>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Tap for more details',
|
"backup_upload_details_page_more_details".t(context: context),
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
@@ -239,14 +239,20 @@ class FileDetailDialog extends ConsumerWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (asset != null) ...[
|
if (asset != null) ...[
|
||||||
_buildInfoSection(context, [
|
_buildInfoSection(context, [
|
||||||
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)),
|
_buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)),
|
||||||
_buildInfoRow(context, "Local ID", asset.id),
|
_buildInfoRow(context, "local_id".t(context: context), asset.id),
|
||||||
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)),
|
_buildInfoRow(
|
||||||
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"),
|
context,
|
||||||
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"),
|
"file_size".t(context: context),
|
||||||
_buildInfoRow(context, "Created At", asset.createdAt.toString()),
|
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||||
_buildInfoRow(context, "Updated At", asset.updatedAt.toString()),
|
),
|
||||||
if (asset.checksum != null) _buildInfoRow(context, "Checksum", asset.checksum!),
|
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(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(album.name),
|
title: Text(album.name),
|
||||||
actions: [const LikeActivityActionButton(menuItem: true)],
|
actions: [const LikeActivityActionButton(iconOnly: true)],
|
||||||
actionsPadding: const EdgeInsets.only(right: 8),
|
actionsPadding: const EdgeInsets.only(right: 8),
|
||||||
),
|
),
|
||||||
body: activities.widgetWhen(
|
body: activities.widgetWhen(
|
||||||
|
|||||||
@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
bool isAlbumTitleTextFieldFocus = false;
|
bool isAlbumTitleTextFieldFocus = false;
|
||||||
Set<BaseAsset> selectedAssets = {};
|
Set<BaseAsset> selectedAssets = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
albumTitleController.addListener(_onTitleChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTitleChanged() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
albumTitleController.removeListener(_onTitleChanged);
|
||||||
albumTitleController.dispose();
|
albumTitleController.dispose();
|
||||||
albumDescriptionController.dispose();
|
albumDescriptionController.dispose();
|
||||||
albumTitleTextFieldFocusNode.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/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
|
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
|
||||||
|
import 'package:immich_ui/immich_ui.dart';
|
||||||
|
|
||||||
/// A widget for cropping an image.
|
/// A widget for cropping an image.
|
||||||
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
|
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows
|
||||||
@@ -30,11 +31,13 @@ class DriftCropImagePage extends HookWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: context.scaffoldBackgroundColor,
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
title: Text("crop".tr()),
|
title: Text("crop".tr()),
|
||||||
leading: CloseButton(color: context.primaryColor),
|
leading: const ImmichCloseButton(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
ImmichIconButton(
|
||||||
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
|
icon: Icons.done_rounded,
|
||||||
onPressed: () async {
|
color: ImmichColor.primary,
|
||||||
|
variant: ImmichVariant.ghost,
|
||||||
|
onTap: () async {
|
||||||
final croppedImage = await cropController.croppedImage();
|
final croppedImage = await cropController.croppedImage();
|
||||||
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
|
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
|
||||||
},
|
},
|
||||||
@@ -72,17 +75,17 @@ class DriftCropImagePage extends HookWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
ImmichIconButton(
|
||||||
icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color),
|
icon: Icons.rotate_left,
|
||||||
onPressed: () {
|
variant: ImmichVariant.ghost,
|
||||||
cropController.rotateLeft();
|
color: ImmichColor.secondary,
|
||||||
},
|
onTap: () => cropController.rotateLeft(),
|
||||||
),
|
),
|
||||||
IconButton(
|
ImmichIconButton(
|
||||||
icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color),
|
icon: Icons.rotate_right,
|
||||||
onPressed: () {
|
variant: ImmichVariant.ghost,
|
||||||
cropController.rotateRight();
|
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 }
|
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
||||||
|
|
||||||
class AddActionButton extends ConsumerWidget {
|
class AddActionButton extends ConsumerStatefulWidget {
|
||||||
const AddActionButton({super.key});
|
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);
|
final asset = ref.read(currentAssetNotifier);
|
||||||
if (asset == null) return;
|
if (asset == null) return [];
|
||||||
|
|
||||||
final user = ref.read(currentUserProvider);
|
final user = ref.read(currentUserProvider);
|
||||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
@@ -35,84 +59,50 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
final hasRemote = asset is RemoteAsset;
|
final hasRemote = asset is RemoteAsset;
|
||||||
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
||||||
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
||||||
final menuItemHeight = 30.0;
|
|
||||||
|
|
||||||
final List<PopupMenuEntry<AddToMenuItem>> items = [
|
return [
|
||||||
PopupMenuItem(
|
Padding(
|
||||||
enabled: false,
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
textStyle: context.textTheme.labelMedium,
|
child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
|
||||||
height: 40,
|
|
||||||
child: Text("add_to_bottom_bar".tr()),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.photo_album_outlined,
|
||||||
value: AddToMenuItem.album,
|
label: "album".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("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) ...[
|
if (isOwner) ...[
|
||||||
|
const Divider(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||||
|
),
|
||||||
if (showArchive)
|
if (showArchive)
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.archive_outlined,
|
||||||
value: AddToMenuItem.archive,
|
label: "archive".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())),
|
menuItem: true,
|
||||||
|
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
|
||||||
),
|
),
|
||||||
if (showUnarchive)
|
if (showUnarchive)
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.unarchive_outlined,
|
||||||
value: AddToMenuItem.unarchive,
|
label: "unarchive".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())),
|
menuItem: true,
|
||||||
|
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
BaseActionButton(
|
||||||
height: menuItemHeight,
|
iconData: Icons.lock_outline,
|
||||||
value: AddToMenuItem.lockedFolder,
|
label: "locked_folder".tr(),
|
||||||
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("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) {
|
void _openAlbumSelector() {
|
||||||
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) {
|
|
||||||
final currentAsset = ref.read(currentAssetNotifier);
|
final currentAsset = ref.read(currentAssetNotifier);
|
||||||
if (currentAsset == null) {
|
if (currentAsset == null) {
|
||||||
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
|
||||||
@@ -120,7 +110,8 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final List<Widget> slivers = [
|
final List<Widget> slivers = [
|
||||||
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)),
|
const CreateAlbumButton(),
|
||||||
|
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)),
|
||||||
];
|
];
|
||||||
|
|
||||||
showModalBottomSheet(
|
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);
|
final latest = ref.read(currentAssetNotifier);
|
||||||
|
|
||||||
if (latest == null) {
|
if (latest == null) {
|
||||||
@@ -165,6 +156,9 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
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) {
|
if (!context.mounted) {
|
||||||
@@ -174,17 +168,38 @@ class AddActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final asset = ref.watch(currentAssetNotifier);
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
return const SizedBox.shrink();
|
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(
|
return BaseActionButton(
|
||||||
iconData: Icons.add,
|
iconData: Icons.add,
|
||||||
label: "add_to_bottom_bar".tr(),
|
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 {
|
class AdvancedInfoActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -26,6 +28,8 @@ class AdvancedInfoActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 115.0,
|
maxWidth: 115.0,
|
||||||
iconData: Icons.help_outline_rounded,
|
iconData: Icons.help_outline_rounded,
|
||||||
label: "troubleshoot".t(context: context),
|
label: "troubleshoot".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required
|
|||||||
|
|
||||||
class ArchiveActionButton extends ConsumerWidget {
|
class ArchiveActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
await performArchiveAction(context, ref, source: source);
|
await performArchiveAction(context, ref, source: source);
|
||||||
@@ -47,6 +49,8 @@ class ArchiveActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.archive_outlined,
|
iconData: Icons.archive_outlined,
|
||||||
label: "to_archive".t(context: context),
|
label: "to_archive".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class BaseActionButton extends StatelessWidget {
|
class BaseActionButton extends ConsumerWidget {
|
||||||
const BaseActionButton({
|
const BaseActionButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
@@ -11,6 +12,7 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
this.onLongPressed,
|
this.onLongPressed,
|
||||||
this.maxWidth = 90.0,
|
this.maxWidth = 90.0,
|
||||||
this.minWidth,
|
this.minWidth,
|
||||||
|
this.iconOnly = false,
|
||||||
this.menuItem = false,
|
this.menuItem = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,25 +21,42 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
final double? minWidth;
|
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 bool menuItem;
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
final void Function()? onLongPressed;
|
final void Function()? onLongPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||||
final iconTheme = IconTheme.of(context);
|
final iconTheme = IconTheme.of(context);
|
||||||
final iconSize = iconTheme.size ?? 24.0;
|
final iconSize = iconTheme.size ?? 24.0;
|
||||||
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
|
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
|
||||||
final textColor = context.themeData.textTheme.labelLarge?.color;
|
final textColor = context.themeData.textTheme.labelLarge?.color;
|
||||||
|
|
||||||
if (menuItem) {
|
if (iconOnly) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
icon: Icon(iconData, size: iconSize, color: iconColor),
|
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(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
child: MaterialButton(
|
child: MaterialButton(
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart';
|
|||||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||||
|
|
||||||
class CastActionButton extends ConsumerWidget {
|
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;
|
final bool menuItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(context: context, builder: (context) => const CastDialog());
|
showDialog(context: context, builder: (context) => const CastDialog());
|
||||||
},
|
},
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,15 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
class DeleteActionButton extends ConsumerWidget {
|
class DeleteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
final bool showConfirmation;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -74,6 +82,8 @@ class DeleteActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 110.0,
|
maxWidth: 110.0,
|
||||||
iconData: Icons.delete_sweep_outlined,
|
iconData: Icons.delete_sweep_outlined,
|
||||||
label: "delete".t(context: context),
|
label: "delete".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
/// - Prompt to delete the asset locally
|
/// - Prompt to delete the asset locally
|
||||||
class DeleteLocalActionButton extends ConsumerWidget {
|
class DeleteLocalActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -55,6 +57,8 @@ class DeleteLocalActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 95.0,
|
maxWidth: 95.0,
|
||||||
iconData: Icons.no_cell_outlined,
|
iconData: Icons.no_cell_outlined,
|
||||||
label: "control_bottom_app_bar_delete_from_local".t(context: context),
|
label: "control_bottom_app_bar_delete_from_local".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
/// - Prompt to delete the asset locally
|
/// - Prompt to delete the asset locally
|
||||||
class DeletePermanentActionButton extends ConsumerWidget {
|
class DeletePermanentActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -51,6 +53,8 @@ class DeletePermanentActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 110.0,
|
maxWidth: 110.0,
|
||||||
iconData: Icons.delete_forever,
|
iconData: Icons.delete_forever,
|
||||||
label: "delete_permanently".t(context: context),
|
label: "delete_permanently".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|||||||
|
|
||||||
class DownloadActionButton extends ConsumerWidget {
|
class DownloadActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.download,
|
iconData: Icons.download,
|
||||||
maxWidth: 95,
|
maxWidth: 95,
|
||||||
label: "download".t(context: context),
|
label: "download".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref, backgroundManager),
|
onPressed: () => _onTap(context, ref, backgroundManager),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class FavoriteActionButton extends ConsumerWidget {
|
class FavoriteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.favorite_border_rounded,
|
iconData: Icons.favorite_border_rounded,
|
||||||
label: "favorite".t(context: context),
|
label: "favorite".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
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';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
class LikeActivityActionButton extends ConsumerWidget {
|
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;
|
final bool menuItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -46,17 +47,19 @@ class LikeActivityActionButton extends ConsumerWidget {
|
|||||||
|
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
maxWidth: 60,
|
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),
|
label: "like".t(context: context),
|
||||||
onPressed: () => onTap(liked),
|
onPressed: () => onTap(liked),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// default to empty heart during loading
|
// default to empty heart during loading
|
||||||
loading: () => BaseActionButton(
|
loading: () => BaseActionButton(
|
||||||
iconData: Icons.favorite_border,
|
iconData: Icons.thumb_up_off_alt,
|
||||||
label: "like".t(context: context),
|
label: "like".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),
|
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';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
|
|
||||||
class MotionPhotoActionButton extends ConsumerWidget {
|
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;
|
final bool menuItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
|
|||||||
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
|
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
|
||||||
label: "play_motion_photo".t(context: context),
|
label: "play_motion_photo".t(context: context),
|
||||||
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref,
|
|||||||
|
|
||||||
class MoveToLockFolderActionButton extends ConsumerWidget {
|
class MoveToLockFolderActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
await performMoveToLockFolderAction(context, ref, source: source);
|
await performMoveToLockFolderAction(context, ref, source: source);
|
||||||
@@ -51,6 +53,8 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 115.0,
|
maxWidth: 115.0,
|
||||||
iconData: Icons.lock_outline_rounded,
|
iconData: Icons.lock_outline_rounded,
|
||||||
label: "move_to_locked_folder".t(context: context),
|
label: "move_to_locked_folder".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.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/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.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 {
|
class RemoveFromAlbumActionButton extends ConsumerWidget {
|
||||||
final String albumId;
|
final String albumId;
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -22,6 +32,10 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
|
|||||||
final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId);
|
final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId);
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
if (source == ActionSource.viewer) {
|
||||||
|
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||||
|
}
|
||||||
|
|
||||||
final successMessage = 'remove_from_album_action_prompt'.t(
|
final successMessage = 'remove_from_album_action_prompt'.t(
|
||||||
context: context,
|
context: context,
|
||||||
args: {'count': result.count.toString()},
|
args: {'count': result.count.toString()},
|
||||||
@@ -42,6 +56,8 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.remove_circle_outline,
|
iconData: Icons.remove_circle_outline,
|
||||||
label: "remove_from_album".t(context: context),
|
label: "remove_from_album".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
maxWidth: 100,
|
maxWidth: 100,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -42,6 +49,8 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 100.0,
|
maxWidth: 100.0,
|
||||||
iconData: Icons.lock_open_rounded,
|
iconData: Icons.lock_open_rounded,
|
||||||
label: "remove_from_locked_folder".t(context: context),
|
label: "remove_from_locked_folder".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ class _SharePreparingDialog extends StatelessWidget {
|
|||||||
|
|
||||||
class ShareActionButton extends ConsumerWidget {
|
class ShareActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -74,6 +76,8 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||||
label: 'share'.t(context: context),
|
label: 'share'.t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
|||||||
|
|
||||||
class ShareLinkActionButton extends ConsumerWidget {
|
class ShareLinkActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
_onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -23,6 +25,8 @@ class ShareLinkActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.link_rounded,
|
iconData: Icons.link_rounded,
|
||||||
label: "share_link".t(context: context),
|
label: "share_link".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
|
|
||||||
class SimilarPhotosActionButton extends ConsumerWidget {
|
class SimilarPhotosActionButton extends ConsumerWidget {
|
||||||
final String assetId;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -44,6 +46,8 @@ class SimilarPhotosActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.compare,
|
iconData: Icons.compare,
|
||||||
label: "view_similar_photos".t(context: context),
|
label: "view_similar_photos".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
maxWidth: 100,
|
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
|
/// which will be permanently deleted after the number of days configure by the admin
|
||||||
class TrashActionButton extends ConsumerWidget {
|
class TrashActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -48,6 +50,8 @@ class TrashActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 85.0,
|
maxWidth: 85.0,
|
||||||
iconData: Icons.delete_outline_rounded,
|
iconData: Icons.delete_outline_rounded,
|
||||||
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
|
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {requir
|
|||||||
|
|
||||||
class UnArchiveActionButton extends ConsumerWidget {
|
class UnArchiveActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
await performUnArchiveAction(context, ref, source: source);
|
await performUnArchiveAction(context, ref, source: source);
|
||||||
@@ -49,6 +51,8 @@ class UnArchiveActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.unarchive_outlined,
|
iconData: Icons.unarchive_outlined,
|
||||||
label: "unarchive".t(context: context),
|
label: "unarchive".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class UnFavoriteActionButton extends ConsumerWidget {
|
class UnFavoriteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.favorite_rounded,
|
iconData: Icons.favorite_rounded,
|
||||||
label: "unfavorite".t(context: context),
|
label: "unfavorite".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class UnStackActionButton extends ConsumerWidget {
|
class UnStackActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -38,6 +40,8 @@ class UnStackActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.layers_clear_outlined,
|
iconData: Icons.layers_clear_outlined,
|
||||||
label: "unstack".t(context: context),
|
label: "unstack".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|||||||
|
|
||||||
class UploadActionButton extends ConsumerWidget {
|
class UploadActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
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 {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@@ -39,6 +41,8 @@ class UploadActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.backup_outlined,
|
iconData: Icons.backup_outlined,
|
||||||
label: "upload".t(context: context),
|
label: "upload".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
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/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.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/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/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.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/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.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,
|
expand: false,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
resizeOnScroll: 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 _kBottomSheetMinimumExtent = 0.4;
|
||||||
const double _kBottomSheetSnapExtent = 0.7;
|
const double _kBottomSheetSnapExtent = 0.67;
|
||||||
|
|
||||||
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
static final _dummyListener = ImageStreamListener((image, _) => image.dispose());
|
static final _dummyListener = ImageStreamListener((image, _) => image.dispose());
|
||||||
@@ -399,10 +399,14 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
final isDraggingDown = currentExtent < previousExtent;
|
final isDraggingDown = currentExtent < previousExtent;
|
||||||
previousExtent = currentExtent;
|
previousExtent = currentExtent;
|
||||||
// Closes the bottom sheet if the user is dragging down
|
// Closes the bottom sheet if the user is dragging down
|
||||||
if (isDraggingDown && delta.extent < 0.55) {
|
if (isDraggingDown && delta.extent < 0.67) {
|
||||||
if (dragInProgress) {
|
if (dragInProgress) {
|
||||||
blockGestures = true;
|
blockGestures = true;
|
||||||
}
|
}
|
||||||
|
// Jump to a lower position before starting close animation to prevent glitch
|
||||||
|
if (bottomSheetController.isAttached) {
|
||||||
|
bottomSheetController.jumpTo(0.67);
|
||||||
|
}
|
||||||
sheetCloseController?.close();
|
sheetCloseController?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +484,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
previousExtent = _kBottomSheetMinimumExtent;
|
previousExtent = _kBottomSheetMinimumExtent;
|
||||||
sheetCloseController = showBottomSheet(
|
sheetCloseController = showBottomSheet(
|
||||||
context: ctx,
|
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),
|
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20.0))),
|
||||||
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
|
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
|
||||||
@@ -688,17 +692,21 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
),
|
),
|
||||||
],
|
if (!showingBottomSheet)
|
||||||
),
|
const Positioned(
|
||||||
bottomNavigationBar: showingBottomSheet
|
bottom: 0,
|
||||||
? const SizedBox.shrink()
|
left: 0,
|
||||||
: const Column(
|
right: 0,
|
||||||
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [AssetStackRow(), ViewerBottomBar()],
|
children: [AssetStackRow(), ViewerBottomBar()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,17 +38,22 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
opacity = 0;
|
opacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final originalTheme = context.themeData;
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
|
|
||||||
|
if (!isInLockedView) ...[
|
||||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
if (asset.type == AssetType.image) const EditImageActionButton(),
|
||||||
if (asset.hasRemote) const AddActionButton(),
|
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||||
|
|
||||||
if (isOwner) ...[
|
if (isOwner) ...[
|
||||||
asset.isLocalOnly
|
asset.isLocalOnly
|
||||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
@@ -74,7 +79,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (asset.isVideo) const VideoControls(),
|
if (asset.isVideo) const VideoControls(),
|
||||||
if (!isInLockedView && !isReadonlyModeEnabled)
|
if (!isReadonlyModeEnabled)
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
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/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.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/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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_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/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.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/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/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.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/bytes_units.dart';
|
||||||
import 'package:immich_mobile/utils/timezone.dart';
|
import 'package:immich_mobile/utils/timezone.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@@ -48,29 +42,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
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(
|
return BaseBottomSheet(
|
||||||
actions: actions,
|
actions: [],
|
||||||
slivers: const [_AssetDetailBottomSheet()],
|
slivers: const [_AssetDetailBottomSheet()],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
initialChildSize: initialChildSize,
|
initialChildSize: initialChildSize,
|
||||||
@@ -79,7 +52,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
expand: false,
|
expand: false,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
resizeOnScroll: 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)
|
// Appears in (Albums)
|
||||||
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||||
// padding at the bottom to avoid cut-off
|
// 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/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.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/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/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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/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/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/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/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/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/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.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/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/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
|
|
||||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
const ViewerTopAppBar({super.key});
|
const ViewerTopAppBar({super.key});
|
||||||
@@ -41,15 +35,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
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));
|
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
@@ -62,11 +47,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
opacity = 0;
|
opacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final originalTheme = context.themeData;
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
|
||||||
if (album != null && album.isActivityEnabled && album.isShared)
|
if (album != null && album.isActivityEnabled && album.isShared)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.chat_outlined),
|
icon: const Icon(Icons.chat_outlined),
|
||||||
@@ -74,28 +58,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
|
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)
|
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)
|
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||||
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
|
|
||||||
const _KebabMenu(),
|
ViewerKebabMenu(originalTheme: originalTheme),
|
||||||
];
|
];
|
||||||
|
|
||||||
final lockedViewActions = <Widget>[
|
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
|
||||||
const _KebabMenu(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: opacity < 255,
|
ignoring: opacity < 255,
|
||||||
@@ -122,20 +94,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
Size get preferredSize => const Size.fromHeight(60.0);
|
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 {
|
class _AppBarBackButton extends ConsumerWidget {
|
||||||
const _AppBarBackButton();
|
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/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.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/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/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.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');
|
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;
|
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/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/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/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/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.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.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
const SliverPersistentHeader(delegate: _DragHandleDelegate(), pinned: true),
|
const SliverToBoxAdapter(child: _DragHandle()),
|
||||||
if (widget.actions.isNotEmpty)
|
if (widget.actions.isNotEmpty)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
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 {
|
class _DragHandle extends StatelessWidget {
|
||||||
const _DragHandle();
|
const _DragHandle();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 50,
|
height: 38,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 32,
|
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/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/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/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/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.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.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
slivers: multiselect.hasRemote
|
slivers: multiselect.hasRemote
|
||||||
? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)]
|
? [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/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/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/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/album/album_selector.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.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.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
if (multiselect.hasLocal) ...[
|
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||||
],
|
],
|
||||||
slivers: ownsAlbum
|
slivers: ownsAlbum
|
||||||
|
|||||||
@@ -324,7 +324,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
|
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
|
||||||
|
|
||||||
const scrubberBottomPadding = 100.0;
|
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(
|
final grid = CustomScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
@@ -347,7 +351,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
addRepaintBoundaries: false,
|
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 {
|
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
|
||||||
final dto = await checkNull(_api.updatePerson(id, PersonUpdateDto(name: name, birthDate: birthday)));
|
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
|
||||||
return _toPerson(dto);
|
final dto = PersonUpdateDto(name: name, birthDate: birthdayUtc);
|
||||||
|
final response = await checkNull(_api.updatePerson(id, dto));
|
||||||
|
return _toPerson(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PersonDto _toPerson(PersonResponseDto dto) => PersonDto(
|
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/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/settings/sync_status.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/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/main_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.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/download_info.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_album.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: LoginRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: ChangePasswordRoute.page),
|
AutoRoute(page: ChangePasswordRoute.page),
|
||||||
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
|
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
|
||||||
CustomRoute(
|
AutoRoute(
|
||||||
page: TabControllerRoute.page,
|
page: TabControllerRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
children: [
|
children: [
|
||||||
@@ -176,9 +176,8 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
|
||||||
),
|
),
|
||||||
CustomRoute(
|
AutoRoute(
|
||||||
page: TabShellRoute.page,
|
page: TabShellRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
children: [
|
children: [
|
||||||
@@ -187,7 +186,6 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
|
||||||
),
|
),
|
||||||
CustomRoute(
|
CustomRoute(
|
||||||
page: GalleryViewerRoute.page,
|
page: GalleryViewerRoute.page,
|
||||||
@@ -288,7 +286,6 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
|
AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: FeatInDevRoute.page, guards: [_authGuard, _duplicateGuard]),
|
|
||||||
AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DriftBackupRoute.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: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DownloadInfoRoute.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
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
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
|
/// generated route for
|
||||||
/// [FilterImagePage]
|
/// [FilterImagePage]
|
||||||
class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
|
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
|
/// generated route for
|
||||||
/// [LibraryPage]
|
/// [LibraryPage]
|
||||||
class LibraryRoute extends PageRouteInfo<void> {
|
class LibraryRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ enum AppSettingsEnum<T> {
|
|||||||
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
|
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
|
||||||
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
|
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
|
||||||
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
|
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
|
||||||
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 0),
|
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
|
||||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||||
@@ -42,7 +42,7 @@ enum AppSettingsEnum<T> {
|
|||||||
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
|
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
|
||||||
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
|
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
|
||||||
ignoreIcloudAssets<bool>(StoreKey.ignoreIcloudAssets, 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),
|
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||||
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, 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/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.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/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/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/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_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_local_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_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/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/unstack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_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 {
|
class ActionButtonContext {
|
||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
@@ -30,6 +39,9 @@ class ActionButtonContext {
|
|||||||
final RemoteAlbum? currentAlbum;
|
final RemoteAlbum? currentAlbum;
|
||||||
final bool advancedTroubleshooting;
|
final bool advancedTroubleshooting;
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool isCasting;
|
||||||
|
final TimelineOrigin timelineOrigin;
|
||||||
|
final ThemeData? originalTheme;
|
||||||
|
|
||||||
const ActionButtonContext({
|
const ActionButtonContext({
|
||||||
required this.asset,
|
required this.asset,
|
||||||
@@ -41,27 +53,33 @@ class ActionButtonContext {
|
|||||||
required this.currentAlbum,
|
required this.currentAlbum,
|
||||||
required this.advancedTroubleshooting,
|
required this.advancedTroubleshooting,
|
||||||
required this.source,
|
required this.source,
|
||||||
|
this.isCasting = false,
|
||||||
|
this.timelineOrigin = TimelineOrigin.main,
|
||||||
|
this.originalTheme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ActionButtonType {
|
enum ActionButtonType {
|
||||||
advancedInfo,
|
openInfo,
|
||||||
|
likeActivity,
|
||||||
share,
|
share,
|
||||||
shareLink,
|
shareLink,
|
||||||
|
cast,
|
||||||
similarPhotos,
|
similarPhotos,
|
||||||
|
viewInTimeline,
|
||||||
|
download,
|
||||||
|
upload,
|
||||||
|
unstack,
|
||||||
archive,
|
archive,
|
||||||
unarchive,
|
unarchive,
|
||||||
download,
|
|
||||||
trash,
|
|
||||||
deletePermanent,
|
|
||||||
delete,
|
|
||||||
moveToLockFolder,
|
moveToLockFolder,
|
||||||
removeFromLockFolder,
|
removeFromLockFolder,
|
||||||
deleteLocal,
|
|
||||||
upload,
|
|
||||||
removeFromAlbum,
|
removeFromAlbum,
|
||||||
unstack,
|
trash,
|
||||||
likeActivity;
|
deleteLocal,
|
||||||
|
deletePermanent,
|
||||||
|
delete,
|
||||||
|
advancedInfo;
|
||||||
|
|
||||||
bool shouldShow(ActionButtonContext context) {
|
bool shouldShow(ActionButtonContext context) {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
@@ -128,39 +146,165 @@ enum ActionButtonType {
|
|||||||
ActionButtonType.similarPhotos =>
|
ActionButtonType.similarPhotos =>
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
context.asset is RemoteAsset,
|
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) {
|
return switch (this) {
|
||||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
ActionButtonType.advancedInfo => AdvancedInfoActionButton(
|
||||||
ActionButtonType.share => ShareActionButton(source: context.source),
|
source: context.source,
|
||||||
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
|
iconOnly: iconOnly,
|
||||||
ActionButtonType.archive => ArchiveActionButton(source: context.source),
|
menuItem: menuItem,
|
||||||
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
|
),
|
||||||
ActionButtonType.download => DownloadActionButton(source: context.source),
|
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.trash => TrashActionButton(source: context.source),
|
ActionButtonType.shareLink => ShareLinkActionButton(
|
||||||
ActionButtonType.deletePermanent => DeletePermanentActionButton(source: context.source),
|
source: context.source,
|
||||||
ActionButtonType.delete => DeleteActionButton(source: context.source),
|
iconOnly: iconOnly,
|
||||||
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(source: context.source),
|
menuItem: menuItem,
|
||||||
ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(source: context.source),
|
),
|
||||||
ActionButtonType.deleteLocal => DeleteLocalActionButton(source: context.source),
|
ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.upload => UploadActionButton(source: context.source),
|
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(
|
ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton(
|
||||||
albumId: context.currentAlbum!.id,
|
albumId: context.currentAlbum!.id,
|
||||||
source: context.source,
|
source: context.source,
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
ActionButtonType.likeActivity => const LikeActivityActionButton(),
|
ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.unstack => UnStackActionButton(source: context.source),
|
ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.similarPhotos => SimilarPhotosActionButton(assetId: (context.asset as RemoteAsset).id),
|
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 {
|
class ActionButtonBuilder {
|
||||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
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) {
|
static List<Widget> build(ActionButtonContext context) {
|
||||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
@@ -68,11 +69,11 @@ class ActivityTextField extends HookConsumerWidget {
|
|||||||
suffixIcon: Padding(
|
suffixIcon: Padding(
|
||||||
padding: const EdgeInsets.only(right: 10),
|
padding: const EdgeInsets.only(right: 10),
|
||||||
child: IconButton(
|
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,
|
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(),
|
hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(),
|
||||||
hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]),
|
hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ActivityTile extends HookConsumerWidget {
|
|||||||
? Container(
|
? Container(
|
||||||
width: isBottomSheet ? 30 : 44,
|
width: isBottomSheet ? 30 : 44,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Icon(Icons.favorite_rounded, color: Colors.red[700]),
|
child: Icon(Icons.thumb_up, color: context.primaryColor),
|
||||||
)
|
)
|
||||||
: isBottomSheet
|
: isBottomSheet
|
||||||
? UserCircleAvatar(user: activity.user, size: 30, radius: 15)
|
? UserCircleAvatar(user: activity.user, size: 30, radius: 15)
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ class CommentBubble extends ConsumerWidget {
|
|||||||
bottom: 6,
|
bottom: 6,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
|
decoration: BoxDecoration(color: context.colorScheme.surfaceContainer, shape: BoxShape.circle),
|
||||||
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
|
child: Icon(Icons.thumb_up, color: context.primaryColor, size: 18),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -81,8 +81,8 @@ class CommentBubble extends ConsumerWidget {
|
|||||||
if (isLike && !showThumbnail) {
|
if (isLike && !showThumbnail) {
|
||||||
likes = Container(
|
likes = Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
|
decoration: BoxDecoration(color: context.colorScheme.surfaceContainer, shape: BoxShape.circle),
|
||||||
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
|
child: Icon(Icons.thumb_up, color: context.primaryColor, size: 18),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,16 +53,18 @@ class ServerUpdateNotification extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
serverInfoState.versionStatus.message,
|
serverInfoState.versionStatus.message,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: context.textTheme.labelLarge,
|
style: context.textTheme.labelLarge,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
|
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
|
||||||
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
|
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
|
||||||
const Spacer(),
|
const SizedBox(width: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: openUpdateLink,
|
onPressed: openUpdateLink,
|
||||||
style: TextButton.styleFrom(
|
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)),
|
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||||
if (kDebugMode || kProfileMode)
|
if (kDebugMode || kProfileMode)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.science_rounded),
|
icon: const Icon(Icons.palette_rounded),
|
||||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
|
||||||
),
|
),
|
||||||
if (isCasting)
|
if (isCasting)
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
|
||||||
if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled)
|
if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.science_rounded),
|
icon: const Icon(Icons.palette_rounded),
|
||||||
onPressed: () => context.pushRoute(const FeatInDevRoute()),
|
onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
|
||||||
),
|
),
|
||||||
if (showUploadButton && !isReadonlyModeEnabled)
|
if (showUploadButton && !isReadonlyModeEnabled)
|
||||||
const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),
|
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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
|
||||||
class EntitiyCountTile extends StatelessWidget {
|
class EntityCountTile extends StatelessWidget {
|
||||||
final int count;
|
final int count;
|
||||||
final String label;
|
final String label;
|
||||||
final IconData icon;
|
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) {
|
String zeroPadding(int number, int targetWidth) {
|
||||||
final numStr = number.toString();
|
final numStr = number.toString();
|
||||||
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
|
height: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surfaceContainerLow,
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
@@ -29,7 +30,6 @@ class EntitiyCountTile extends StatelessWidget {
|
|||||||
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
|
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Icon and Label
|
// Icon and Label
|
||||||
@@ -38,18 +38,17 @@ class EntitiyCountTile extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(icon, color: context.primaryColor),
|
Icon(icon, color: context.primaryColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Flexible(
|
||||||
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
// Number
|
// Number
|
||||||
LayoutBuilder(
|
const Spacer(),
|
||||||
builder: (context, constraints) {
|
RichText(
|
||||||
final maxDigits = calculateMaxDigits(constraints.maxWidth);
|
|
||||||
return RichText(
|
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
||||||
children: [
|
children: [
|
||||||
@@ -63,8 +62,6 @@ class EntitiyCountTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -282,20 +282,24 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
_SectionHeaderText(text: "assets".t(context: context)),
|
_SectionHeaderText(text: "assets".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
// 1. Wrap in IntrinsicHeight
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// 2. Stretch children vertically to fill the IntrinsicHeight
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "local".t(context: context),
|
label: "local".t(context: context),
|
||||||
count: localAssetCount,
|
count: localAssetCount,
|
||||||
icon: Icons.smartphone,
|
icon: Icons.smartphone,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "remote".t(context: context),
|
label: "remote".t(context: context),
|
||||||
count: remoteAssetCount,
|
count: remoteAssetCount,
|
||||||
icon: Icons.cloud,
|
icon: Icons.cloud,
|
||||||
@@ -304,23 +308,26 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
_SectionHeaderText(text: "albums".t(context: context)),
|
_SectionHeaderText(text: "albums".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "local".t(context: context),
|
label: "local".t(context: context),
|
||||||
count: localAlbumCount,
|
count: localAlbumCount,
|
||||||
icon: Icons.smartphone,
|
icon: Icons.smartphone,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "remote".t(context: context),
|
label: "remote".t(context: context),
|
||||||
count: remoteAlbumCount,
|
count: remoteAlbumCount,
|
||||||
icon: Icons.cloud,
|
icon: Icons.cloud,
|
||||||
@@ -329,23 +336,26 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
_SectionHeaderText(text: "other".t(context: context)),
|
_SectionHeaderText(text: "other".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "memories".t(context: context),
|
label: "memories".t(context: context),
|
||||||
count: memoryCount,
|
count: memoryCount,
|
||||||
icon: Icons.calendar_today,
|
icon: Icons.calendar_today,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "hashed_assets".t(context: context),
|
label: "hashed_assets".t(context: context),
|
||||||
count: localHashedCount,
|
count: localHashedCount,
|
||||||
icon: Icons.tag,
|
icon: Icons.tag,
|
||||||
@@ -354,6 +364,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// To be removed once the experimental feature is stable
|
// To be removed once the experimental feature is stable
|
||||||
if (CurrentPlatform.isAndroid &&
|
if (CurrentPlatform.isAndroid &&
|
||||||
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
||||||
@@ -364,20 +375,22 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
return counts.when(
|
return counts.when(
|
||||||
data: (c) => Padding(
|
data: (c) => Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "local".t(context: context),
|
label: "local".t(context: context),
|
||||||
count: c.total,
|
count: c.total,
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "hashed_assets".t(context: context),
|
label: "hashed_assets".t(context: context),
|
||||||
count: c.hashed,
|
count: c.hashed,
|
||||||
icon: Icons.tag,
|
icon: Icons.tag,
|
||||||
@@ -386,6 +399,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const CircularProgressIndicator(),
|
||||||
error: (e, st) => Text('Error: $e'),
|
error: (e, st) => Text('Error: $e'),
|
||||||
);
|
);
|
||||||
|
|||||||
13
mobile/openapi/README.md
generated
13
mobile/openapi/README.md
generated
@@ -133,6 +133,11 @@ Class | Method | HTTP request | Description
|
|||||||
*AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | Unlock auth session
|
*AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | Unlock auth session
|
||||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | Validate access token
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | Validate access token
|
||||||
*AuthenticationAdminApi* | [**unlinkAllOAuthAccountsAdmin**](doc//AuthenticationAdminApi.md#unlinkalloauthaccountsadmin) | **POST** /admin/auth/unlink-all | Unlink all OAuth accounts
|
*AuthenticationAdminApi* | [**unlinkAllOAuthAccountsAdmin**](doc//AuthenticationAdminApi.md#unlinkalloauthaccountsadmin) | **POST** /admin/auth/unlink-all | Unlink all OAuth accounts
|
||||||
|
*DatabaseBackupsAdminApi* | [**deleteDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#deletedatabasebackup) | **DELETE** /admin/database-backups | Delete database backup
|
||||||
|
*DatabaseBackupsAdminApi* | [**downloadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#downloaddatabasebackup) | **GET** /admin/database-backups/{filename} | Download database backup
|
||||||
|
*DatabaseBackupsAdminApi* | [**listDatabaseBackups**](doc//DatabaseBackupsAdminApi.md#listdatabasebackups) | **GET** /admin/database-backups | List database backups
|
||||||
|
*DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow
|
||||||
|
*DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup
|
||||||
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner
|
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner
|
||||||
*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID
|
*DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID
|
||||||
*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user
|
*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user
|
||||||
@@ -161,6 +166,8 @@ Class | Method | HTTP request | Description
|
|||||||
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library
|
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library
|
||||||
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
|
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
|
||||||
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
|
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
|
||||||
|
*MaintenanceAdminApi* | [**detectPriorInstall**](doc//MaintenanceAdminApi.md#detectpriorinstall) | **GET** /admin/maintenance/detect-install | Detect existing install
|
||||||
|
*MaintenanceAdminApi* | [**getMaintenanceStatus**](doc//MaintenanceAdminApi.md#getmaintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status
|
||||||
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
|
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
|
||||||
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
|
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
|
||||||
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
|
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
|
||||||
@@ -387,6 +394,8 @@ Class | Method | HTTP request | Description
|
|||||||
- [CreateLibraryDto](doc//CreateLibraryDto.md)
|
- [CreateLibraryDto](doc//CreateLibraryDto.md)
|
||||||
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
||||||
- [DatabaseBackupConfig](doc//DatabaseBackupConfig.md)
|
- [DatabaseBackupConfig](doc//DatabaseBackupConfig.md)
|
||||||
|
- [DatabaseBackupDeleteDto](doc//DatabaseBackupDeleteDto.md)
|
||||||
|
- [DatabaseBackupListResponseDto](doc//DatabaseBackupListResponseDto.md)
|
||||||
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
|
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
|
||||||
- [DownloadInfoDto](doc//DownloadInfoDto.md)
|
- [DownloadInfoDto](doc//DownloadInfoDto.md)
|
||||||
- [DownloadResponse](doc//DownloadResponse.md)
|
- [DownloadResponse](doc//DownloadResponse.md)
|
||||||
@@ -416,7 +425,10 @@ Class | Method | HTTP request | Description
|
|||||||
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
|
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
|
||||||
- [MaintenanceAction](doc//MaintenanceAction.md)
|
- [MaintenanceAction](doc//MaintenanceAction.md)
|
||||||
- [MaintenanceAuthDto](doc//MaintenanceAuthDto.md)
|
- [MaintenanceAuthDto](doc//MaintenanceAuthDto.md)
|
||||||
|
- [MaintenanceDetectInstallResponseDto](doc//MaintenanceDetectInstallResponseDto.md)
|
||||||
|
- [MaintenanceDetectInstallStorageFolderDto](doc//MaintenanceDetectInstallStorageFolderDto.md)
|
||||||
- [MaintenanceLoginDto](doc//MaintenanceLoginDto.md)
|
- [MaintenanceLoginDto](doc//MaintenanceLoginDto.md)
|
||||||
|
- [MaintenanceStatusResponseDto](doc//MaintenanceStatusResponseDto.md)
|
||||||
- [ManualJobName](doc//ManualJobName.md)
|
- [ManualJobName](doc//ManualJobName.md)
|
||||||
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||||
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
|
||||||
@@ -528,6 +540,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [StackResponseDto](doc//StackResponseDto.md)
|
- [StackResponseDto](doc//StackResponseDto.md)
|
||||||
- [StackUpdateDto](doc//StackUpdateDto.md)
|
- [StackUpdateDto](doc//StackUpdateDto.md)
|
||||||
- [StatisticsSearchDto](doc//StatisticsSearchDto.md)
|
- [StatisticsSearchDto](doc//StatisticsSearchDto.md)
|
||||||
|
- [StorageFolder](doc//StorageFolder.md)
|
||||||
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
|
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
|
||||||
- [SyncAckDto](doc//SyncAckDto.md)
|
- [SyncAckDto](doc//SyncAckDto.md)
|
||||||
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
||||||
|
|||||||
7
mobile/openapi/lib/api.dart
generated
7
mobile/openapi/lib/api.dart
generated
@@ -36,6 +36,7 @@ part 'api/albums_api.dart';
|
|||||||
part 'api/assets_api.dart';
|
part 'api/assets_api.dart';
|
||||||
part 'api/authentication_api.dart';
|
part 'api/authentication_api.dart';
|
||||||
part 'api/authentication_admin_api.dart';
|
part 'api/authentication_admin_api.dart';
|
||||||
|
part 'api/database_backups_admin_api.dart';
|
||||||
part 'api/deprecated_api.dart';
|
part 'api/deprecated_api.dart';
|
||||||
part 'api/download_api.dart';
|
part 'api/download_api.dart';
|
||||||
part 'api/duplicates_api.dart';
|
part 'api/duplicates_api.dart';
|
||||||
@@ -139,6 +140,8 @@ part 'model/create_album_dto.dart';
|
|||||||
part 'model/create_library_dto.dart';
|
part 'model/create_library_dto.dart';
|
||||||
part 'model/create_profile_image_response_dto.dart';
|
part 'model/create_profile_image_response_dto.dart';
|
||||||
part 'model/database_backup_config.dart';
|
part 'model/database_backup_config.dart';
|
||||||
|
part 'model/database_backup_delete_dto.dart';
|
||||||
|
part 'model/database_backup_list_response_dto.dart';
|
||||||
part 'model/download_archive_info.dart';
|
part 'model/download_archive_info.dart';
|
||||||
part 'model/download_info_dto.dart';
|
part 'model/download_info_dto.dart';
|
||||||
part 'model/download_response.dart';
|
part 'model/download_response.dart';
|
||||||
@@ -168,7 +171,10 @@ part 'model/logout_response_dto.dart';
|
|||||||
part 'model/machine_learning_availability_checks_dto.dart';
|
part 'model/machine_learning_availability_checks_dto.dart';
|
||||||
part 'model/maintenance_action.dart';
|
part 'model/maintenance_action.dart';
|
||||||
part 'model/maintenance_auth_dto.dart';
|
part 'model/maintenance_auth_dto.dart';
|
||||||
|
part 'model/maintenance_detect_install_response_dto.dart';
|
||||||
|
part 'model/maintenance_detect_install_storage_folder_dto.dart';
|
||||||
part 'model/maintenance_login_dto.dart';
|
part 'model/maintenance_login_dto.dart';
|
||||||
|
part 'model/maintenance_status_response_dto.dart';
|
||||||
part 'model/manual_job_name.dart';
|
part 'model/manual_job_name.dart';
|
||||||
part 'model/map_marker_response_dto.dart';
|
part 'model/map_marker_response_dto.dart';
|
||||||
part 'model/map_reverse_geocode_response_dto.dart';
|
part 'model/map_reverse_geocode_response_dto.dart';
|
||||||
@@ -280,6 +286,7 @@ part 'model/stack_create_dto.dart';
|
|||||||
part 'model/stack_response_dto.dart';
|
part 'model/stack_response_dto.dart';
|
||||||
part 'model/stack_update_dto.dart';
|
part 'model/stack_update_dto.dart';
|
||||||
part 'model/statistics_search_dto.dart';
|
part 'model/statistics_search_dto.dart';
|
||||||
|
part 'model/storage_folder.dart';
|
||||||
part 'model/sync_ack_delete_dto.dart';
|
part 'model/sync_ack_delete_dto.dart';
|
||||||
part 'model/sync_ack_dto.dart';
|
part 'model/sync_ack_dto.dart';
|
||||||
part 'model/sync_ack_set_dto.dart';
|
part 'model/sync_ack_set_dto.dart';
|
||||||
|
|||||||
269
mobile/openapi/lib/api/database_backups_admin_api.dart
generated
Normal file
269
mobile/openapi/lib/api/database_backups_admin_api.dart
generated
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseBackupsAdminApi {
|
||||||
|
DatabaseBackupsAdminApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Delete database backup
|
||||||
|
///
|
||||||
|
/// Delete a backup by its filename
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required):
|
||||||
|
Future<Response> deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = databaseBackupDeleteDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete database backup
|
||||||
|
///
|
||||||
|
/// Delete a backup by its filename
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required):
|
||||||
|
Future<void> deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async {
|
||||||
|
final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download database backup
|
||||||
|
///
|
||||||
|
/// Downloads the database backup file
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] filename (required):
|
||||||
|
Future<Response> downloadDatabaseBackupWithHttpInfo(String filename,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups/{filename}'
|
||||||
|
.replaceAll('{filename}', filename);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download database backup
|
||||||
|
///
|
||||||
|
/// Downloads the database backup file
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] filename (required):
|
||||||
|
Future<MultipartFile?> downloadDatabaseBackup(String filename,) async {
|
||||||
|
final response = await downloadDatabaseBackupWithHttpInfo(filename,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List database backups
|
||||||
|
///
|
||||||
|
/// Get the list of the successful and failed backups
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> listDatabaseBackupsWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List database backups
|
||||||
|
///
|
||||||
|
/// Get the list of the successful and failed backups
|
||||||
|
Future<DatabaseBackupListResponseDto?> listDatabaseBackups() async {
|
||||||
|
final response = await listDatabaseBackupsWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'DatabaseBackupListResponseDto',) as DatabaseBackupListResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start database backup restore flow
|
||||||
|
///
|
||||||
|
/// Put Immich into maintenance mode to restore a backup (Immich must not be configured)
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> startDatabaseRestoreFlowWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups/start-restore';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start database backup restore flow
|
||||||
|
///
|
||||||
|
/// Put Immich into maintenance mode to restore a backup (Immich must not be configured)
|
||||||
|
Future<void> startDatabaseRestoreFlow() async {
|
||||||
|
final response = await startDatabaseRestoreFlowWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload database backup
|
||||||
|
///
|
||||||
|
/// Uploads .sql/.sql.gz file to restore backup from
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MultipartFile] file:
|
||||||
|
Future<Response> uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/database-backups/upload';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['multipart/form-data'];
|
||||||
|
|
||||||
|
bool hasFields = false;
|
||||||
|
final mp = MultipartRequest('POST', Uri.parse(apiPath));
|
||||||
|
if (file != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'file'] = file.field;
|
||||||
|
mp.files.add(file);
|
||||||
|
}
|
||||||
|
if (hasFields) {
|
||||||
|
postBody = mp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload database backup
|
||||||
|
///
|
||||||
|
/// Uploads .sql/.sql.gz file to restore backup from
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MultipartFile] file:
|
||||||
|
Future<void> uploadDatabaseBackup({ MultipartFile? file, }) async {
|
||||||
|
final response = await uploadDatabaseBackupWithHttpInfo( file: file, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
mobile/openapi/lib/api/maintenance_admin_api.dart
generated
96
mobile/openapi/lib/api/maintenance_admin_api.dart
generated
@@ -16,6 +16,102 @@ class MaintenanceAdminApi {
|
|||||||
|
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Detect existing install
|
||||||
|
///
|
||||||
|
/// Collect integrity checks and other heuristics about local data.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> detectPriorInstallWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/maintenance/detect-install';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect existing install
|
||||||
|
///
|
||||||
|
/// Collect integrity checks and other heuristics about local data.
|
||||||
|
Future<MaintenanceDetectInstallResponseDto?> detectPriorInstall() async {
|
||||||
|
final response = await detectPriorInstallWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceDetectInstallResponseDto',) as MaintenanceDetectInstallResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get maintenance mode status
|
||||||
|
///
|
||||||
|
/// Fetch information about the currently running maintenance action.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> getMaintenanceStatusWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/maintenance/status';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get maintenance mode status
|
||||||
|
///
|
||||||
|
/// Fetch information about the currently running maintenance action.
|
||||||
|
Future<MaintenanceStatusResponseDto?> getMaintenanceStatus() async {
|
||||||
|
final response = await getMaintenanceStatusWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceStatusResponseDto',) as MaintenanceStatusResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Log into maintenance mode
|
/// Log into maintenance mode
|
||||||
///
|
///
|
||||||
/// Login with maintenance token or cookie to receive current information and perform further actions.
|
/// Login with maintenance token or cookie to receive current information and perform further actions.
|
||||||
|
|||||||
12
mobile/openapi/lib/api_client.dart
generated
12
mobile/openapi/lib/api_client.dart
generated
@@ -326,6 +326,10 @@ class ApiClient {
|
|||||||
return CreateProfileImageResponseDto.fromJson(value);
|
return CreateProfileImageResponseDto.fromJson(value);
|
||||||
case 'DatabaseBackupConfig':
|
case 'DatabaseBackupConfig':
|
||||||
return DatabaseBackupConfig.fromJson(value);
|
return DatabaseBackupConfig.fromJson(value);
|
||||||
|
case 'DatabaseBackupDeleteDto':
|
||||||
|
return DatabaseBackupDeleteDto.fromJson(value);
|
||||||
|
case 'DatabaseBackupListResponseDto':
|
||||||
|
return DatabaseBackupListResponseDto.fromJson(value);
|
||||||
case 'DownloadArchiveInfo':
|
case 'DownloadArchiveInfo':
|
||||||
return DownloadArchiveInfo.fromJson(value);
|
return DownloadArchiveInfo.fromJson(value);
|
||||||
case 'DownloadInfoDto':
|
case 'DownloadInfoDto':
|
||||||
@@ -384,8 +388,14 @@ class ApiClient {
|
|||||||
return MaintenanceActionTypeTransformer().decode(value);
|
return MaintenanceActionTypeTransformer().decode(value);
|
||||||
case 'MaintenanceAuthDto':
|
case 'MaintenanceAuthDto':
|
||||||
return MaintenanceAuthDto.fromJson(value);
|
return MaintenanceAuthDto.fromJson(value);
|
||||||
|
case 'MaintenanceDetectInstallResponseDto':
|
||||||
|
return MaintenanceDetectInstallResponseDto.fromJson(value);
|
||||||
|
case 'MaintenanceDetectInstallStorageFolderDto':
|
||||||
|
return MaintenanceDetectInstallStorageFolderDto.fromJson(value);
|
||||||
case 'MaintenanceLoginDto':
|
case 'MaintenanceLoginDto':
|
||||||
return MaintenanceLoginDto.fromJson(value);
|
return MaintenanceLoginDto.fromJson(value);
|
||||||
|
case 'MaintenanceStatusResponseDto':
|
||||||
|
return MaintenanceStatusResponseDto.fromJson(value);
|
||||||
case 'ManualJobName':
|
case 'ManualJobName':
|
||||||
return ManualJobNameTypeTransformer().decode(value);
|
return ManualJobNameTypeTransformer().decode(value);
|
||||||
case 'MapMarkerResponseDto':
|
case 'MapMarkerResponseDto':
|
||||||
@@ -608,6 +618,8 @@ class ApiClient {
|
|||||||
return StackUpdateDto.fromJson(value);
|
return StackUpdateDto.fromJson(value);
|
||||||
case 'StatisticsSearchDto':
|
case 'StatisticsSearchDto':
|
||||||
return StatisticsSearchDto.fromJson(value);
|
return StatisticsSearchDto.fromJson(value);
|
||||||
|
case 'StorageFolder':
|
||||||
|
return StorageFolderTypeTransformer().decode(value);
|
||||||
case 'SyncAckDeleteDto':
|
case 'SyncAckDeleteDto':
|
||||||
return SyncAckDeleteDto.fromJson(value);
|
return SyncAckDeleteDto.fromJson(value);
|
||||||
case 'SyncAckDto':
|
case 'SyncAckDto':
|
||||||
|
|||||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -157,6 +157,9 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is SourceType) {
|
if (value is SourceType) {
|
||||||
return SourceTypeTypeTransformer().encode(value).toString();
|
return SourceTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
if (value is StorageFolder) {
|
||||||
|
return StorageFolderTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
if (value is SyncEntityType) {
|
if (value is SyncEntityType) {
|
||||||
return SyncEntityTypeTypeTransformer().encode(value).toString();
|
return SyncEntityTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
101
mobile/openapi/lib/model/database_backup_delete_dto.dart
generated
Normal file
101
mobile/openapi/lib/model/database_backup_delete_dto.dart
generated
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class DatabaseBackupDeleteDto {
|
||||||
|
/// Returns a new [DatabaseBackupDeleteDto] instance.
|
||||||
|
DatabaseBackupDeleteDto({
|
||||||
|
this.backups = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> backups;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDeleteDto &&
|
||||||
|
_deepEquality.equals(other.backups, backups);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(backups.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'DatabaseBackupDeleteDto[backups=$backups]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'backups'] = this.backups;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [DatabaseBackupDeleteDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static DatabaseBackupDeleteDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "DatabaseBackupDeleteDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return DatabaseBackupDeleteDto(
|
||||||
|
backups: json[r'backups'] is Iterable
|
||||||
|
? (json[r'backups'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DatabaseBackupDeleteDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <DatabaseBackupDeleteDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = DatabaseBackupDeleteDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, DatabaseBackupDeleteDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, DatabaseBackupDeleteDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = DatabaseBackupDeleteDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of DatabaseBackupDeleteDto-objects as value to a dart map
|
||||||
|
static Map<String, List<DatabaseBackupDeleteDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<DatabaseBackupDeleteDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = DatabaseBackupDeleteDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'backups',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
101
mobile/openapi/lib/model/database_backup_list_response_dto.dart
generated
Normal file
101
mobile/openapi/lib/model/database_backup_list_response_dto.dart
generated
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class DatabaseBackupListResponseDto {
|
||||||
|
/// Returns a new [DatabaseBackupListResponseDto] instance.
|
||||||
|
DatabaseBackupListResponseDto({
|
||||||
|
this.backups = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> backups;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupListResponseDto &&
|
||||||
|
_deepEquality.equals(other.backups, backups);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(backups.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'DatabaseBackupListResponseDto[backups=$backups]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'backups'] = this.backups;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [DatabaseBackupListResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static DatabaseBackupListResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "DatabaseBackupListResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return DatabaseBackupListResponseDto(
|
||||||
|
backups: json[r'backups'] is Iterable
|
||||||
|
? (json[r'backups'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DatabaseBackupListResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <DatabaseBackupListResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = DatabaseBackupListResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, DatabaseBackupListResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, DatabaseBackupListResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = DatabaseBackupListResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of DatabaseBackupListResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<DatabaseBackupListResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<DatabaseBackupListResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = DatabaseBackupListResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'backups',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
3
mobile/openapi/lib/model/maintenance_action.dart
generated
3
mobile/openapi/lib/model/maintenance_action.dart
generated
@@ -25,11 +25,13 @@ class MaintenanceAction {
|
|||||||
|
|
||||||
static const start = MaintenanceAction._(r'start');
|
static const start = MaintenanceAction._(r'start');
|
||||||
static const end = MaintenanceAction._(r'end');
|
static const end = MaintenanceAction._(r'end');
|
||||||
|
static const restoreDatabase = MaintenanceAction._(r'restore_database');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][MaintenanceAction].
|
/// List of all possible values in this [enum][MaintenanceAction].
|
||||||
static const values = <MaintenanceAction>[
|
static const values = <MaintenanceAction>[
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
restoreDatabase,
|
||||||
];
|
];
|
||||||
|
|
||||||
static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value);
|
static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value);
|
||||||
@@ -70,6 +72,7 @@ class MaintenanceActionTypeTransformer {
|
|||||||
switch (data) {
|
switch (data) {
|
||||||
case r'start': return MaintenanceAction.start;
|
case r'start': return MaintenanceAction.start;
|
||||||
case r'end': return MaintenanceAction.end;
|
case r'end': return MaintenanceAction.end;
|
||||||
|
case r'restore_database': return MaintenanceAction.restoreDatabase;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
99
mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart
generated
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class MaintenanceDetectInstallResponseDto {
|
||||||
|
/// Returns a new [MaintenanceDetectInstallResponseDto] instance.
|
||||||
|
MaintenanceDetectInstallResponseDto({
|
||||||
|
this.storage = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<MaintenanceDetectInstallStorageFolderDto> storage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallResponseDto &&
|
||||||
|
_deepEquality.equals(other.storage, storage);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(storage.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MaintenanceDetectInstallResponseDto[storage=$storage]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'storage'] = this.storage;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MaintenanceDetectInstallResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MaintenanceDetectInstallResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MaintenanceDetectInstallResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MaintenanceDetectInstallResponseDto(
|
||||||
|
storage: MaintenanceDetectInstallStorageFolderDto.listFromJson(json[r'storage']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MaintenanceDetectInstallResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MaintenanceDetectInstallResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MaintenanceDetectInstallResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MaintenanceDetectInstallResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MaintenanceDetectInstallResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MaintenanceDetectInstallResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MaintenanceDetectInstallResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MaintenanceDetectInstallResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MaintenanceDetectInstallResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MaintenanceDetectInstallResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'storage',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
123
mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart
generated
Normal file
123
mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart
generated
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class MaintenanceDetectInstallStorageFolderDto {
|
||||||
|
/// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance.
|
||||||
|
MaintenanceDetectInstallStorageFolderDto({
|
||||||
|
required this.files,
|
||||||
|
required this.folder,
|
||||||
|
required this.readable,
|
||||||
|
required this.writable,
|
||||||
|
});
|
||||||
|
|
||||||
|
num files;
|
||||||
|
|
||||||
|
StorageFolder folder;
|
||||||
|
|
||||||
|
bool readable;
|
||||||
|
|
||||||
|
bool writable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallStorageFolderDto &&
|
||||||
|
other.files == files &&
|
||||||
|
other.folder == folder &&
|
||||||
|
other.readable == readable &&
|
||||||
|
other.writable == writable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(files.hashCode) +
|
||||||
|
(folder.hashCode) +
|
||||||
|
(readable.hashCode) +
|
||||||
|
(writable.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MaintenanceDetectInstallStorageFolderDto[files=$files, folder=$folder, readable=$readable, writable=$writable]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'files'] = this.files;
|
||||||
|
json[r'folder'] = this.folder;
|
||||||
|
json[r'readable'] = this.readable;
|
||||||
|
json[r'writable'] = this.writable;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MaintenanceDetectInstallStorageFolderDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MaintenanceDetectInstallStorageFolderDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MaintenanceDetectInstallStorageFolderDto(
|
||||||
|
files: num.parse('${json[r'files']}'),
|
||||||
|
folder: StorageFolder.fromJson(json[r'folder'])!,
|
||||||
|
readable: mapValueOfType<bool>(json, r'readable')!,
|
||||||
|
writable: mapValueOfType<bool>(json, r'writable')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MaintenanceDetectInstallStorageFolderDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MaintenanceDetectInstallStorageFolderDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MaintenanceDetectInstallStorageFolderDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MaintenanceDetectInstallStorageFolderDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MaintenanceDetectInstallStorageFolderDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MaintenanceDetectInstallStorageFolderDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MaintenanceDetectInstallStorageFolderDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MaintenanceDetectInstallStorageFolderDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MaintenanceDetectInstallStorageFolderDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MaintenanceDetectInstallStorageFolderDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'files',
|
||||||
|
'folder',
|
||||||
|
'readable',
|
||||||
|
'writable',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
158
mobile/openapi/lib/model/maintenance_status_response_dto.dart
generated
Normal file
158
mobile/openapi/lib/model/maintenance_status_response_dto.dart
generated
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class MaintenanceStatusResponseDto {
|
||||||
|
/// Returns a new [MaintenanceStatusResponseDto] instance.
|
||||||
|
MaintenanceStatusResponseDto({
|
||||||
|
required this.action,
|
||||||
|
required this.active,
|
||||||
|
this.error,
|
||||||
|
this.progress,
|
||||||
|
this.task,
|
||||||
|
});
|
||||||
|
|
||||||
|
MaintenanceAction action;
|
||||||
|
|
||||||
|
bool active;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? progress;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? task;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is MaintenanceStatusResponseDto &&
|
||||||
|
other.action == action &&
|
||||||
|
other.active == active &&
|
||||||
|
other.error == error &&
|
||||||
|
other.progress == progress &&
|
||||||
|
other.task == task;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(action.hashCode) +
|
||||||
|
(active.hashCode) +
|
||||||
|
(error == null ? 0 : error!.hashCode) +
|
||||||
|
(progress == null ? 0 : progress!.hashCode) +
|
||||||
|
(task == null ? 0 : task!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MaintenanceStatusResponseDto[action=$action, active=$active, error=$error, progress=$progress, task=$task]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'action'] = this.action;
|
||||||
|
json[r'active'] = this.active;
|
||||||
|
if (this.error != null) {
|
||||||
|
json[r'error'] = this.error;
|
||||||
|
} else {
|
||||||
|
// json[r'error'] = null;
|
||||||
|
}
|
||||||
|
if (this.progress != null) {
|
||||||
|
json[r'progress'] = this.progress;
|
||||||
|
} else {
|
||||||
|
// json[r'progress'] = null;
|
||||||
|
}
|
||||||
|
if (this.task != null) {
|
||||||
|
json[r'task'] = this.task;
|
||||||
|
} else {
|
||||||
|
// json[r'task'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [MaintenanceStatusResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static MaintenanceStatusResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "MaintenanceStatusResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return MaintenanceStatusResponseDto(
|
||||||
|
action: MaintenanceAction.fromJson(json[r'action'])!,
|
||||||
|
active: mapValueOfType<bool>(json, r'active')!,
|
||||||
|
error: mapValueOfType<String>(json, r'error'),
|
||||||
|
progress: num.parse('${json[r'progress']}'),
|
||||||
|
task: mapValueOfType<String>(json, r'task'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MaintenanceStatusResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <MaintenanceStatusResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = MaintenanceStatusResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, MaintenanceStatusResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, MaintenanceStatusResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = MaintenanceStatusResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of MaintenanceStatusResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<MaintenanceStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<MaintenanceStatusResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = MaintenanceStatusResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'action',
|
||||||
|
'active',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
12
mobile/openapi/lib/model/permission.dart
generated
12
mobile/openapi/lib/model/permission.dart
generated
@@ -58,6 +58,10 @@ class Permission {
|
|||||||
static const authPeriodChangePassword = Permission._(r'auth.changePassword');
|
static const authPeriodChangePassword = Permission._(r'auth.changePassword');
|
||||||
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
|
static const authDevicePeriodDelete = Permission._(r'authDevice.delete');
|
||||||
static const archivePeriodRead = Permission._(r'archive.read');
|
static const archivePeriodRead = Permission._(r'archive.read');
|
||||||
|
static const backupPeriodList = Permission._(r'backup.list');
|
||||||
|
static const backupPeriodDownload = Permission._(r'backup.download');
|
||||||
|
static const backupPeriodUpload = Permission._(r'backup.upload');
|
||||||
|
static const backupPeriodDelete = Permission._(r'backup.delete');
|
||||||
static const duplicatePeriodRead = Permission._(r'duplicate.read');
|
static const duplicatePeriodRead = Permission._(r'duplicate.read');
|
||||||
static const duplicatePeriodDelete = Permission._(r'duplicate.delete');
|
static const duplicatePeriodDelete = Permission._(r'duplicate.delete');
|
||||||
static const facePeriodCreate = Permission._(r'face.create');
|
static const facePeriodCreate = Permission._(r'face.create');
|
||||||
@@ -206,6 +210,10 @@ class Permission {
|
|||||||
authPeriodChangePassword,
|
authPeriodChangePassword,
|
||||||
authDevicePeriodDelete,
|
authDevicePeriodDelete,
|
||||||
archivePeriodRead,
|
archivePeriodRead,
|
||||||
|
backupPeriodList,
|
||||||
|
backupPeriodDownload,
|
||||||
|
backupPeriodUpload,
|
||||||
|
backupPeriodDelete,
|
||||||
duplicatePeriodRead,
|
duplicatePeriodRead,
|
||||||
duplicatePeriodDelete,
|
duplicatePeriodDelete,
|
||||||
facePeriodCreate,
|
facePeriodCreate,
|
||||||
@@ -389,6 +397,10 @@ class PermissionTypeTransformer {
|
|||||||
case r'auth.changePassword': return Permission.authPeriodChangePassword;
|
case r'auth.changePassword': return Permission.authPeriodChangePassword;
|
||||||
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
|
case r'authDevice.delete': return Permission.authDevicePeriodDelete;
|
||||||
case r'archive.read': return Permission.archivePeriodRead;
|
case r'archive.read': return Permission.archivePeriodRead;
|
||||||
|
case r'backup.list': return Permission.backupPeriodList;
|
||||||
|
case r'backup.download': return Permission.backupPeriodDownload;
|
||||||
|
case r'backup.upload': return Permission.backupPeriodUpload;
|
||||||
|
case r'backup.delete': return Permission.backupPeriodDelete;
|
||||||
case r'duplicate.read': return Permission.duplicatePeriodRead;
|
case r'duplicate.read': return Permission.duplicatePeriodRead;
|
||||||
case r'duplicate.delete': return Permission.duplicatePeriodDelete;
|
case r'duplicate.delete': return Permission.duplicatePeriodDelete;
|
||||||
case r'face.create': return Permission.facePeriodCreate;
|
case r'face.create': return Permission.facePeriodCreate;
|
||||||
|
|||||||
@@ -14,25 +14,41 @@ class SetMaintenanceModeDto {
|
|||||||
/// Returns a new [SetMaintenanceModeDto] instance.
|
/// Returns a new [SetMaintenanceModeDto] instance.
|
||||||
SetMaintenanceModeDto({
|
SetMaintenanceModeDto({
|
||||||
required this.action,
|
required this.action,
|
||||||
|
this.restoreBackupFilename,
|
||||||
});
|
});
|
||||||
|
|
||||||
MaintenanceAction action;
|
MaintenanceAction action;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? restoreBackupFilename;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto &&
|
||||||
other.action == action;
|
other.action == action &&
|
||||||
|
other.restoreBackupFilename == restoreBackupFilename;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(action.hashCode);
|
(action.hashCode) +
|
||||||
|
(restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SetMaintenanceModeDto[action=$action]';
|
String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'action'] = this.action;
|
json[r'action'] = this.action;
|
||||||
|
if (this.restoreBackupFilename != null) {
|
||||||
|
json[r'restoreBackupFilename'] = this.restoreBackupFilename;
|
||||||
|
} else {
|
||||||
|
// json[r'restoreBackupFilename'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +62,7 @@ class SetMaintenanceModeDto {
|
|||||||
|
|
||||||
return SetMaintenanceModeDto(
|
return SetMaintenanceModeDto(
|
||||||
action: MaintenanceAction.fromJson(json[r'action'])!,
|
action: MaintenanceAction.fromJson(json[r'action'])!,
|
||||||
|
restoreBackupFilename: mapValueOfType<String>(json, r'restoreBackupFilename'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
97
mobile/openapi/lib/model/storage_folder.dart
generated
Normal file
97
mobile/openapi/lib/model/storage_folder.dart
generated
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class StorageFolder {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const StorageFolder._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const encodedVideo = StorageFolder._(r'encoded-video');
|
||||||
|
static const library_ = StorageFolder._(r'library');
|
||||||
|
static const upload = StorageFolder._(r'upload');
|
||||||
|
static const profile = StorageFolder._(r'profile');
|
||||||
|
static const thumbs = StorageFolder._(r'thumbs');
|
||||||
|
static const backups = StorageFolder._(r'backups');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][StorageFolder].
|
||||||
|
static const values = <StorageFolder>[
|
||||||
|
encodedVideo,
|
||||||
|
library_,
|
||||||
|
upload,
|
||||||
|
profile,
|
||||||
|
thumbs,
|
||||||
|
backups,
|
||||||
|
];
|
||||||
|
|
||||||
|
static StorageFolder? fromJson(dynamic value) => StorageFolderTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<StorageFolder> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <StorageFolder>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = StorageFolder.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [StorageFolder] to String,
|
||||||
|
/// and [decode] dynamic data back to [StorageFolder].
|
||||||
|
class StorageFolderTypeTransformer {
|
||||||
|
factory StorageFolderTypeTransformer() => _instance ??= const StorageFolderTypeTransformer._();
|
||||||
|
|
||||||
|
const StorageFolderTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(StorageFolder data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a StorageFolder.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
StorageFolder? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'encoded-video': return StorageFolder.encodedVideo;
|
||||||
|
case r'library': return StorageFolder.library_;
|
||||||
|
case r'upload': return StorageFolder.upload;
|
||||||
|
case r'profile': return StorageFolder.profile;
|
||||||
|
case r'thumbs': return StorageFolder.thumbs;
|
||||||
|
case r'backups': return StorageFolder.backups;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [StorageFolderTypeTransformer] instance.
|
||||||
|
static StorageFolderTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
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';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user