Compare commits
60 Commits
feat/group
...
feat/beta-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c4b27b94 | ||
|
|
a5760129f0 | ||
|
|
d430b869ac | ||
|
|
4179c8a17d | ||
|
|
0a9cbf01d2 | ||
|
|
9567a2a560 | ||
|
|
58dd6f094c | ||
|
|
02381343ff | ||
|
|
09a5963eee | ||
|
|
a573a23c83 | ||
|
|
7118dca559 | ||
|
|
13d43e193e | ||
|
|
7a7843467c | ||
|
|
9e6fee4064 | ||
|
|
9680f1290d | ||
|
|
ce2ea98926 | ||
|
|
5c76cc34e1 | ||
|
|
eb2f4c866e | ||
|
|
2a370087e8 | ||
|
|
272c8a5812 | ||
|
|
08fe549ed8 | ||
|
|
ae15efdf2a | ||
|
|
8e003f95db | ||
|
|
3e92e837f1 | ||
|
|
081307ced2 | ||
|
|
a91bb399f0 | ||
|
|
42b78c59b5 | ||
|
|
750d21aeba | ||
|
|
990d9ba9a8 | ||
|
|
4d0c9172e5 | ||
|
|
094e3a2757 | ||
|
|
278668b8c5 | ||
|
|
10141504a2 | ||
|
|
67736c8fce | ||
|
|
b56a272f64 | ||
|
|
5901c2e963 | ||
|
|
be85832b20 | ||
|
|
c8f9a72d3e | ||
|
|
3d633a81c4 | ||
|
|
4efbf36d82 | ||
|
|
e2c3c39597 | ||
|
|
007ba1d9ef | ||
|
|
4d5cd1a6b5 | ||
|
|
8108f50c4e | ||
|
|
1b8354ed36 | ||
|
|
9242afb4b0 | ||
|
|
c5f14adff0 | ||
|
|
1378f22368 | ||
|
|
4bd465e752 | ||
|
|
a07531be3b | ||
|
|
3cdc6844a1 | ||
|
|
c3263e50fc | ||
|
|
7391ea6ff9 | ||
|
|
f972b8d514 | ||
|
|
6b50d958f4 | ||
|
|
27c456eb75 | ||
|
|
e7d051db3c | ||
|
|
86d31d7d29 | ||
|
|
f416342eff | ||
|
|
d73335ecbc |
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
96
.github/workflows/close-duplicates.yml
vendored
Normal file
96
.github/workflows/close-duplicates.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
discussion:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
name: Close likely duplicates
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get_body:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
EVENT: ${{ toJSON(github.event) }}
|
||||||
|
outputs:
|
||||||
|
body: ${{ steps.get_body.outputs.body }}
|
||||||
|
steps:
|
||||||
|
- id: get_body
|
||||||
|
run: |
|
||||||
|
BODY=$(echo """$EVENT""" | jq -r '.issue // .discussion | .body' | base64 -w 0)
|
||||||
|
echo "body=$BODY" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
get_checkbox_json:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: get_body
|
||||||
|
container:
|
||||||
|
image: yshavit/mdq:0.7.2
|
||||||
|
outputs:
|
||||||
|
json: ${{ steps.get_checkbox.outputs.json }}
|
||||||
|
steps:
|
||||||
|
- id: get_checkbox
|
||||||
|
env:
|
||||||
|
BODY: ${{ needs.get_body.outputs.body }}
|
||||||
|
run: |
|
||||||
|
JSON=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes')
|
||||||
|
echo "json=$JSON" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
close_and_comment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: get_checkbox_json
|
||||||
|
if: ${{ !fromJSON(needs.get_checkbox_json.outputs.json).items[0].list[0].checked }}
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
discussions: write
|
||||||
|
steps:
|
||||||
|
- name: Close issue
|
||||||
|
if: ${{ github.event_name == 'issues' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
NODE_ID: ${{ github.event.issue.node_id }}
|
||||||
|
run: |
|
||||||
|
gh api graphql \
|
||||||
|
-f issueId="$NODE_ID" \
|
||||||
|
-f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||||
|
-f query='
|
||||||
|
mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
|
||||||
|
addComment(input: {
|
||||||
|
subjectId: $issueId,
|
||||||
|
body: $body
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
|
||||||
|
closeIssue(input: {
|
||||||
|
issueId: $issueId,
|
||||||
|
stateReason: DUPLICATE
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
- name: Close discussion
|
||||||
|
if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
NODE_ID: ${{ github.event.discussion.node_id }}
|
||||||
|
run: |
|
||||||
|
gh api graphql \
|
||||||
|
-f discussionId="$NODE_ID" \
|
||||||
|
-f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||||
|
-f query='
|
||||||
|
mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
|
||||||
|
addDiscussionComment(input: {
|
||||||
|
discussionId: $discussionId,
|
||||||
|
body: $body
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDiscussion(input: {
|
||||||
|
discussionId: $discussionId,
|
||||||
|
reason: DUPLICATE
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}'
|
||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -76,6 +76,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
@@ -129,7 +129,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
178
cli/package-lock.json
generated
178
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.73",
|
"version": "2.2.77",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.73",
|
"version": "2.2.77",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@@ -27,7 +27,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": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@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",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"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": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.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.2.5",
|
||||||
@@ -54,14 +54,14 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.136.0",
|
"version": "1.137.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -90,9 +90,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -632,9 +632,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.14.0",
|
"version": "0.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -682,9 +682,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
|
||||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -705,13 +705,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.15.1",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1355,9 +1355,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.16.5",
|
"version": "22.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1897,9 +1897,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1917,10 +1917,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.173",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.1"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -1981,9 +1981,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001713",
|
"version": "1.0.30001731",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||||
"integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
|
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2035,6 +2035,13 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/check-error": {
|
"node_modules/check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
@@ -2061,9 +2068,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
|
||||||
"integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
|
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2150,13 +2157,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.41.0",
|
"version": "3.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
|
||||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
"integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4"
|
"browserslist": "^4.25.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2221,9 +2228,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.137",
|
"version": "1.5.195",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
|
||||||
"integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==",
|
"integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2306,9 +2313,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
|
||||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2318,8 +2325,8 @@
|
|||||||
"@eslint/config-helpers": "^0.3.0",
|
"@eslint/config-helpers": "^0.3.0",
|
||||||
"@eslint/core": "^0.15.0",
|
"@eslint/core": "^0.15.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.31.0",
|
"@eslint/js": "9.32.0",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.3.4",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
@@ -2414,65 +2421,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "59.0.1",
|
"version": "60.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz",
|
||||||
"integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==",
|
"integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"@eslint-community/eslint-utils": "^4.5.1",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.3.3",
|
||||||
"ci-info": "^4.2.0",
|
"change-case": "^5.4.4",
|
||||||
|
"ci-info": "^4.3.0",
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.41.0",
|
"core-js-compat": "^3.44.0",
|
||||||
"esquery": "^1.6.0",
|
"esquery": "^1.6.0",
|
||||||
"find-up-simple": "^1.0.1",
|
"find-up-simple": "^1.0.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.3.0",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"is-builtin-module": "^5.0.0",
|
"is-builtin-module": "^5.0.0",
|
||||||
"jsesc": "^3.1.0",
|
"jsesc": "^3.1.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.12.0",
|
"regjsparser": "^0.12.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.2",
|
||||||
"strip-indent": "^4.0.0"
|
"strip-indent": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.20.0 || ^20.10.0 || >=21.0.0"
|
"node": "^20.10.0 || >=21.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=9.22.0"
|
"eslint": ">=9.29.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": {
|
|
||||||
"version": "0.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint/core": "^0.13.0",
|
|
||||||
"levn": "^0.4.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@@ -2505,19 +2486,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/@eslint/core": {
|
|
||||||
"version": "0.15.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
|
||||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
@@ -3727,9 +3695,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4211,15 +4179,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
||||||
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
|
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.4.6",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.40.0",
|
"rollup": "^4.40.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.14"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.73",
|
"version": "2.2.77",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
@@ -21,7 +21,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": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@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",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"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": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.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.2.5",
|
||||||
@@ -69,6 +69,6 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.0.2-ubuntu@sha256:0512d81cdeaaff0e370a9aa66027b465d1f1f04379c3a9c801a905fabbdbc7a5
|
image: grafana/grafana:12.1.0-ubuntu@sha256:397aa30dd1af16cb6c5c9879498e467973a7f87eacf949f6d5a29407a3843809
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
||||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (empty for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
||||||
@@ -106,6 +106,89 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
|
|||||||
|
|
||||||
## Example Configuration
|
## Example Configuration
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Authelia Example</summary>
|
||||||
|
|
||||||
|
### Authelia Example
|
||||||
|
|
||||||
|
Here's an example of OAuth configured for Authelia:
|
||||||
|
|
||||||
|
This assumes there exist an attribute `immichquota` in the user schema, which is used to set the user's storage quota in Immich.
|
||||||
|
The configuration concerning the quota is optional.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
authentication_backend:
|
||||||
|
ldap:
|
||||||
|
# The LDAP server configuration goes here.
|
||||||
|
# See: https://www.authelia.com/c/ldap
|
||||||
|
attributes:
|
||||||
|
extra:
|
||||||
|
immichquota: # The attribute name from LDAP
|
||||||
|
name: 'immich_quota'
|
||||||
|
multi_valued: false
|
||||||
|
value_type: 'integer'
|
||||||
|
identity_providers:
|
||||||
|
oidc:
|
||||||
|
## The other portions of the mandatory OpenID Connect 1.0 configuration go here.
|
||||||
|
## See: https://www.authelia.com/c/oidc
|
||||||
|
claims_policies:
|
||||||
|
immich_policy:
|
||||||
|
custom_claims:
|
||||||
|
immich_quota:
|
||||||
|
attribute: 'immich_quota'
|
||||||
|
scopes:
|
||||||
|
immich_scope:
|
||||||
|
claims:
|
||||||
|
- 'immich_quota'
|
||||||
|
|
||||||
|
clients:
|
||||||
|
- client_id: 'immich'
|
||||||
|
client_name: 'Immich'
|
||||||
|
# https://www.authelia.com/integration/openid-connect/frequently-asked-questions/#how-do-i-generate-a-client-identifier-or-client-secret
|
||||||
|
client_secret: $pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng'
|
||||||
|
public: false
|
||||||
|
require_pkce: false
|
||||||
|
redirect_uris:
|
||||||
|
- 'https://example.immich.app/auth/login'
|
||||||
|
- 'https://example.immich.app/user-settings'
|
||||||
|
- 'app.immich:///oauth-callback'
|
||||||
|
scopes:
|
||||||
|
- 'openid'
|
||||||
|
- 'profile'
|
||||||
|
- 'email'
|
||||||
|
- 'immich_scope'
|
||||||
|
claims_policy: 'immich_policy'
|
||||||
|
response_types:
|
||||||
|
- 'code'
|
||||||
|
grant_types:
|
||||||
|
- 'authorization_code'
|
||||||
|
id_token_signed_response_alg: 'RS256'
|
||||||
|
userinfo_signed_response_alg: 'RS256'
|
||||||
|
token_endpoint_auth_method: 'client_secret_post'
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration of OAuth in Immich System Settings
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
| ---------------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| Issuer URL | `https://example.immich.app/.well-known/openid-configuration` |
|
||||||
|
| Client ID | immich |
|
||||||
|
| Client Secret | 0v89FXkQOWO\***\*\*\*\*\***\*\*\***\*\*\*\*\***mprbvXD549HH6s1iw... |
|
||||||
|
| Token Endpoint Auth Method | client_secret_post |
|
||||||
|
| Scope | openid email profile immich_scope |
|
||||||
|
| ID Token Signed Response Algorithm | RS256 |
|
||||||
|
| Userinfo Signed Response Algorithm | RS256 |
|
||||||
|
| Storage Label Claim | uid |
|
||||||
|
| Storage Quota Claim | immich_quota |
|
||||||
|
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
|
||||||
|
| Button Text | Sign in with Authelia (optional) |
|
||||||
|
| Auto Register | Enabled (optional) |
|
||||||
|
| Auto Launch | Enabled (optional) |
|
||||||
|
| Mobile Redirect URI Override | Disable |
|
||||||
|
| Mobile Redirect URI | |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Authentik Example</summary>
|
<summary>Authentik Example</summary>
|
||||||
|
|
||||||
@@ -128,7 +211,7 @@ Configuration of OAuth in Immich System Settings
|
|||||||
| Signing Algorithm | RS256 |
|
| Signing Algorithm | RS256 |
|
||||||
| Storage Label Claim | preferred_username |
|
| Storage Label Claim | preferred_username |
|
||||||
| Storage Quota Claim | immich_quota |
|
| Storage Quota Claim | immich_quota |
|
||||||
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
|
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
|
||||||
| Button Text | Sign in with Authentik (optional) |
|
| Button Text | Sign in with Authentik (optional) |
|
||||||
| Auto Register | Enabled (optional) |
|
| Auto Register | Enabled (optional) |
|
||||||
| Auto Launch | Enabled (optional) |
|
| Auto Launch | Enabled (optional) |
|
||||||
@@ -159,7 +242,7 @@ Configuration of OAuth in Immich System Settings
|
|||||||
| Signing Algorithm | RS256 |
|
| Signing Algorithm | RS256 |
|
||||||
| Storage Label Claim | preferred_username |
|
| Storage Label Claim | preferred_username |
|
||||||
| Storage Quota Claim | immich_quota |
|
| Storage Quota Claim | immich_quota |
|
||||||
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
|
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
|
||||||
| Button Text | Sign in with Google (optional) |
|
| Button Text | Sign in with Google (optional) |
|
||||||
| Auto Register | Enabled (optional) |
|
| Auto Register | Enabled (optional) |
|
||||||
| Auto Launch | Enabled |
|
| Auto Launch | Enabled |
|
||||||
|
|||||||
@@ -59,6 +59,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
docs/static/archived-versions.json
vendored
16
docs/static/archived-versions.json
vendored
@@ -1,4 +1,20 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.137.3",
|
||||||
|
"url": "https://v1.137.3.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.137.2",
|
||||||
|
"url": "https://v1.137.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.137.1",
|
||||||
|
"url": "https://v1.137.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.137.0",
|
||||||
|
"url": "https://v1.137.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.136.0",
|
"label": "v1.136.0",
|
||||||
"url": "https://v1.136.0.archive.immich.app"
|
"url": "https://v1.136.0.archive.immich.app"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.17.1
|
22.18.0
|
||||||
|
|||||||
188
e2e/package-lock.json
generated
188
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.136.0",
|
"version": "1.137.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.136.0",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@@ -16,7 +16,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": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@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",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"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": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.73",
|
"version": "2.2.77",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -68,7 +68,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": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@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",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"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": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.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.2.5",
|
||||||
@@ -95,14 +95,14 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.136.0",
|
"version": "1.137.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -131,9 +131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.25.9",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -684,9 +684,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.14.0",
|
"version": "0.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
||||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -734,9 +734,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
|
||||||
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
|
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -757,13 +757,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.15.1",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1999,9 +1999,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/luxon": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.6.2",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||||
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
|
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2020,9 +2020,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.16.5",
|
"version": "22.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2030,9 +2030,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/oidc-provider": {
|
"node_modules/@types/oidc-provider": {
|
||||||
"version": "9.1.1",
|
"version": "9.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.2.tgz",
|
||||||
"integrity": "sha512-sG4UcE4AbUwAsEpyrcyoqZ383wJiQObZU+gTa1Iv288+l09HwSr88hBZE2IBLlXS+RKmLId0i4B430PBFO/XRA==",
|
"integrity": "sha512-JAreXkbWsZR72Gt3eigG652wq1qBcjhuy421PXU2a8PS0mM00XlG+UdXbM/QPihM3ko0YF8cwvt0H2kacXGcsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2042,9 +2042,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/pg": {
|
"node_modules/@types/pg": {
|
||||||
"version": "8.15.4",
|
"version": "8.15.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
|
||||||
"integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==",
|
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2741,9 +2741,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2761,10 +2761,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.173",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.1"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -2862,9 +2862,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001713",
|
"version": "1.0.30001731",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
|
||||||
"integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
|
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2916,6 +2916,13 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/check-error": {
|
"node_modules/check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
@@ -2937,9 +2944,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
|
||||||
"integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==",
|
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3112,13 +3119,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.41.0",
|
"version": "3.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
|
||||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
"integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4"
|
"browserslist": "^4.25.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3271,9 +3278,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.137",
|
"version": "1.5.195",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
|
||||||
"integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==",
|
"integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -3464,9 +3471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.31.0",
|
"version": "9.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
|
||||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3476,8 +3483,8 @@
|
|||||||
"@eslint/config-helpers": "^0.3.0",
|
"@eslint/config-helpers": "^0.3.0",
|
||||||
"@eslint/core": "^0.15.0",
|
"@eslint/core": "^0.15.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.31.0",
|
"@eslint/js": "9.32.0",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.3.4",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
@@ -3572,65 +3579,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "59.0.1",
|
"version": "60.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz",
|
||||||
"integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==",
|
"integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
"@eslint-community/eslint-utils": "^4.5.1",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.3.3",
|
||||||
"ci-info": "^4.2.0",
|
"change-case": "^5.4.4",
|
||||||
|
"ci-info": "^4.3.0",
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.41.0",
|
"core-js-compat": "^3.44.0",
|
||||||
"esquery": "^1.6.0",
|
"esquery": "^1.6.0",
|
||||||
"find-up-simple": "^1.0.1",
|
"find-up-simple": "^1.0.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.3.0",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"is-builtin-module": "^5.0.0",
|
"is-builtin-module": "^5.0.0",
|
||||||
"jsesc": "^3.1.0",
|
"jsesc": "^3.1.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.12.0",
|
"regjsparser": "^0.12.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.2",
|
||||||
"strip-indent": "^4.0.0"
|
"strip-indent": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.20.0 || ^20.10.0 || >=21.0.0"
|
"node": "^20.10.0 || >=21.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=9.22.0"
|
"eslint": ">=9.29.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": {
|
|
||||||
"version": "0.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
|
||||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": {
|
|
||||||
"version": "0.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint/core": "^0.13.0",
|
|
||||||
"levn": "^0.4.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@@ -3663,19 +3644,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/@eslint/core": {
|
|
||||||
"version": "0.15.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
|
|
||||||
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.136.0",
|
"version": "1.137.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -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": "^22.16.5",
|
"@types/node": "^22.17.0",
|
||||||
"@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",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"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": "^59.0.0",
|
"eslint-plugin-unicorn": "^60.0.0",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
@@ -54,6 +54,6 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.17.1"
|
"node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, shareUrl, utils } from 'src/utils';
|
import { app, asBearerAuth, baseUrl, shareUrl, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -78,6 +78,7 @@ describe('/shared-links', () => {
|
|||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
albumId: metadataAlbum.id,
|
albumId: metadataAlbum.id,
|
||||||
showMetadata: true,
|
showMetadata: true,
|
||||||
|
slug: 'metadata-album',
|
||||||
}),
|
}),
|
||||||
utils.createSharedLink(user1.accessToken, {
|
utils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
@@ -138,6 +139,17 @@ describe('/shared-links', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /s/:slug', () => {
|
||||||
|
it('should work for slug auth', async () => {
|
||||||
|
const resp = await request(baseUrl).get(`/s/${linkWithMetadata.slug}`);
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
|
expect(resp.text).toContain(
|
||||||
|
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET /shared-links', () => {
|
describe('GET /shared-links', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-links');
|
const { status, body } = await request(app).get('/shared-links');
|
||||||
|
|||||||
@@ -653,6 +653,7 @@
|
|||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"clear_all_recent_searches": "Clear all recent searches",
|
"clear_all_recent_searches": "Clear all recent searches",
|
||||||
|
"clear_file_cache": "Clear File Cache",
|
||||||
"clear_message": "Clear message",
|
"clear_message": "Clear message",
|
||||||
"clear_value": "Clear value",
|
"clear_value": "Clear value",
|
||||||
"client_cert_dialog_msg_confirm": "OK",
|
"client_cert_dialog_msg_confirm": "OK",
|
||||||
@@ -723,6 +724,7 @@
|
|||||||
"create_new_user": "Create new user",
|
"create_new_user": "Create new user",
|
||||||
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
||||||
"create_shared_album_page_share_select_photos": "Select Photos",
|
"create_shared_album_page_share_select_photos": "Select Photos",
|
||||||
|
"create_shared_link": "Create shared link",
|
||||||
"create_tag": "Create tag",
|
"create_tag": "Create tag",
|
||||||
"create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
|
"create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
|
||||||
"create_user": "Create user",
|
"create_user": "Create user",
|
||||||
@@ -834,6 +836,7 @@
|
|||||||
"edit_birthday": "Edit Birthday",
|
"edit_birthday": "Edit Birthday",
|
||||||
"edit_date": "Edit date",
|
"edit_date": "Edit date",
|
||||||
"edit_date_and_time": "Edit date and time",
|
"edit_date_and_time": "Edit date and time",
|
||||||
|
"edit_date_and_time_action_prompt": "{count} date and time edited",
|
||||||
"edit_description": "Edit description",
|
"edit_description": "Edit description",
|
||||||
"edit_description_prompt": "Please select a new description:",
|
"edit_description_prompt": "Please select a new description:",
|
||||||
"edit_exclusion_pattern": "Edit exclusion pattern",
|
"edit_exclusion_pattern": "Edit exclusion pattern",
|
||||||
@@ -1171,6 +1174,7 @@
|
|||||||
"latest_version": "Latest Version",
|
"latest_version": "Latest Version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
|
"leave_album": "Leave album",
|
||||||
"lens_model": "Lens model",
|
"lens_model": "Lens model",
|
||||||
"let_others_respond": "Let others respond",
|
"let_others_respond": "Let others respond",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
@@ -1250,7 +1254,7 @@
|
|||||||
"manage_your_devices": "Manage your logged-in devices",
|
"manage_your_devices": "Manage your logged-in devices",
|
||||||
"manage_your_oauth_connection": "Manage your OAuth connection",
|
"manage_your_oauth_connection": "Manage your OAuth connection",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
"map_assets_in_bounds": "{count, plural, one {# photo} other {# photos}}",
|
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
||||||
"map_cannot_get_user_location": "Cannot get user's location",
|
"map_cannot_get_user_location": "Cannot get user's location",
|
||||||
"map_location_dialog_yes": "Yes",
|
"map_location_dialog_yes": "Yes",
|
||||||
"map_location_picker_page_use_location": "Use this location",
|
"map_location_picker_page_use_location": "Use this location",
|
||||||
@@ -1258,7 +1262,6 @@
|
|||||||
"map_location_service_disabled_title": "Location Service disabled",
|
"map_location_service_disabled_title": "Location Service disabled",
|
||||||
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
|
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
|
||||||
"map_marker_with_image": "Map marker with image",
|
"map_marker_with_image": "Map marker with image",
|
||||||
"map_no_assets_in_bounds": "No photos in this area",
|
|
||||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||||
"map_no_location_permission_title": "Location Permission denied",
|
"map_no_location_permission_title": "Location Permission denied",
|
||||||
"map_settings": "Map settings",
|
"map_settings": "Map settings",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
// See also: https://pub.dev/packages/pigeon
|
// See also: https://pub.dev/packages/pigeon
|
||||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
|||||||
@@ -29,21 +29,24 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
||||||
)
|
)
|
||||||
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
|
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
|
||||||
val ASSET_PROJECTION = arrayOf(
|
val ASSET_PROJECTION = buildList {
|
||||||
MediaStore.MediaColumns._ID,
|
add(MediaStore.MediaColumns._ID)
|
||||||
MediaStore.MediaColumns.DATA,
|
add(MediaStore.MediaColumns.DATA)
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
add(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||||
MediaStore.MediaColumns.DATE_TAKEN,
|
add(MediaStore.MediaColumns.DATE_TAKEN)
|
||||||
MediaStore.MediaColumns.DATE_ADDED,
|
add(MediaStore.MediaColumns.DATE_ADDED)
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
add(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
add(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
MediaStore.MediaColumns.BUCKET_ID,
|
add(MediaStore.MediaColumns.BUCKET_ID)
|
||||||
MediaStore.MediaColumns.WIDTH,
|
add(MediaStore.MediaColumns.WIDTH)
|
||||||
MediaStore.MediaColumns.HEIGHT,
|
add(MediaStore.MediaColumns.HEIGHT)
|
||||||
MediaStore.MediaColumns.DURATION,
|
add(MediaStore.MediaColumns.DURATION)
|
||||||
MediaStore.MediaColumns.ORIENTATION,
|
add(MediaStore.MediaColumns.ORIENTATION)
|
||||||
MediaStore.MediaColumns.IS_FAVORITE,
|
// IS_FAVORITE is only available on Android 11 and above
|
||||||
)
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||||
val orientationColumn =
|
val orientationColumn =
|
||||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||||
val favoriteColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.IS_FAVORITE)
|
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
|
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
val id = c.getLong(idColumn).toString()
|
val id = c.getLong(idColumn).toString()
|
||||||
@@ -107,7 +110,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
else c.getLong(durationColumn) / 1000
|
else c.getLong(durationColumn) / 1000
|
||||||
val bucketId = c.getString(bucketIdColumn)
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
val orientation = c.getInt(orientationColumn)
|
val orientation = c.getInt(orientationColumn)
|
||||||
val isFavorite = c.getInt(favoriteColumn) != 0;
|
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||||
|
|
||||||
val asset = PlatformAsset(
|
val asset = PlatformAsset(
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -24,14 +24,23 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
|
|
||||||
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
val serverURL = prefs.getString("widget_server_url", "") ?: ""
|
||||||
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
val sessionKey = prefs.getString("widget_auth_token", "") ?: ""
|
||||||
|
val customHeadersJSON = prefs.getString("widget_custom_headers", "") ?: ""
|
||||||
|
|
||||||
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
if (serverURL.isBlank() || sessionKey.isBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customHeaders: Map<String, String> = HashMap<String, String>()
|
||||||
|
|
||||||
|
if (customHeadersJSON.isNotBlank()) {
|
||||||
|
val stringMapType = object : TypeToken<Map<String, String>>() {}.type
|
||||||
|
customHeaders = Gson().fromJson(customHeadersJSON, stringMapType)
|
||||||
|
}
|
||||||
|
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
serverURL,
|
serverURL,
|
||||||
sessionKey
|
sessionKey,
|
||||||
|
customHeaders
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,11 +59,19 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
return URL(urlString.toString())
|
return URL(urlString.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun HttpURLConnection.applyCustomHeaders() {
|
||||||
|
serverConfig.customHeaders.forEach { (key, value) ->
|
||||||
|
setRequestProperty(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
suspend fun fetchSearchResults(filters: SearchFilters): List<Asset> = withContext(Dispatchers.IO) {
|
||||||
val url = buildRequestURL("/search/random")
|
val url = buildRequestURL("/search/random")
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "POST"
|
requestMethod = "POST"
|
||||||
setRequestProperty("Content-Type", "application/json")
|
setRequestProperty("Content-Type", "application/json")
|
||||||
|
applyCustomHeaders()
|
||||||
|
|
||||||
doOutput = true
|
doOutput = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +92,7 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
val url = buildRequestURL("/memories", listOf("for" to iso8601))
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
|
applyCustomHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
@@ -94,6 +112,7 @@ class ImmichAPI(cfg: ServerConfig) {
|
|||||||
val url = buildRequestURL("/albums")
|
val url = buildRequestURL("/albums")
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
|
applyCustomHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = connection.inputStream.bufferedReader().readText()
|
val response = connection.inputStream.bufferedReader().readText()
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ data class WidgetEntry (
|
|||||||
val deeplink: String?
|
val deeplink: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
data class ServerConfig(
|
||||||
|
val serverEndpoint: String,
|
||||||
|
val sessionKey: String,
|
||||||
|
val customHeaders: Map<String, String>
|
||||||
|
)
|
||||||
|
|
||||||
// MARK: Widget State Keys
|
// MARK: Widget State Keys
|
||||||
val kImageUUID = stringPreferencesKey("uuid")
|
val kImageUUID = stringPreferencesKey("uuid")
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 205,
|
"android.injected.version.code" => 3002,
|
||||||
"android.injected.version.name" => "1.136.0",
|
"android.injected.version.name" => "1.137.3",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -649,7 +649,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -793,7 +793,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -823,7 +823,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -857,7 +857,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -900,7 +900,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -940,7 +940,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -979,7 +979,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1023,7 +1023,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1064,7 +1064,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.135.1</string>
|
<string>1.137.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>210</string>
|
<string>213</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
// See also: https://pub.dev/packages/pigeon
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|||||||
@@ -104,10 +104,13 @@ struct Album: Codable, Equatable {
|
|||||||
// MARK: API
|
// MARK: API
|
||||||
|
|
||||||
class ImmichAPI {
|
class ImmichAPI {
|
||||||
|
typealias CustomHeaders = [String:String]
|
||||||
struct ServerConfig {
|
struct ServerConfig {
|
||||||
let serverEndpoint: String
|
let serverEndpoint: String
|
||||||
let sessionKey: String
|
let sessionKey: String
|
||||||
|
let customHeaders: CustomHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverConfig: ServerConfig
|
let serverConfig: ServerConfig
|
||||||
|
|
||||||
init() async throws {
|
init() async throws {
|
||||||
@@ -122,10 +125,20 @@ class ImmichAPI {
|
|||||||
if serverURL == "" || sessionKey == "" {
|
if serverURL == "" || sessionKey == "" {
|
||||||
throw WidgetError.noLogin
|
throw WidgetError.noLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom headers come in the form of KV pairs in JSON
|
||||||
|
var customHeadersJSON = (defaults.string(forKey: "widget_custom_headers") ?? "")
|
||||||
|
var customHeaders: CustomHeaders = [:]
|
||||||
|
|
||||||
|
if customHeadersJSON != "",
|
||||||
|
let parsedHeaders = try? JSONDecoder().decode(CustomHeaders.self, from: customHeadersJSON.data(using: .utf8)!) {
|
||||||
|
customHeaders = parsedHeaders
|
||||||
|
}
|
||||||
|
|
||||||
serverConfig = ServerConfig(
|
serverConfig = ServerConfig(
|
||||||
serverEndpoint: serverURL,
|
serverEndpoint: serverURL,
|
||||||
sessionKey: sessionKey
|
sessionKey: sessionKey,
|
||||||
|
customHeaders: customHeaders
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +168,12 @@ class ImmichAPI {
|
|||||||
|
|
||||||
return components?.url
|
return components?.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyCustomHeaders(for request: inout URLRequest) {
|
||||||
|
for (header, value) in serverConfig.customHeaders {
|
||||||
|
request.addValue(value, forHTTPHeaderField: header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)
|
||||||
async throws
|
async throws
|
||||||
@@ -174,7 +193,8 @@ class ImmichAPI {
|
|||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.httpBody = try JSONEncoder().encode(filters)
|
request.httpBody = try JSONEncoder().encode(filters)
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
applyCustomHeaders(for: &request)
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
// decode data
|
// decode data
|
||||||
@@ -196,6 +216,7 @@ class ImmichAPI {
|
|||||||
|
|
||||||
var request = URLRequest(url: searchURL)
|
var request = URLRequest(url: searchURL)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
|
applyCustomHeaders(for: &request)
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
@@ -254,7 +275,8 @@ class ImmichAPI {
|
|||||||
|
|
||||||
var request = URLRequest(url: searchURL)
|
var request = URLRequest(url: searchURL)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
|
applyCustomHeaders(for: &request)
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
// decode data
|
// decode data
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ platform :ios do
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
)
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.136.0"
|
version_number: "1.137.3"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ const int kTimelineAssetLoadBatchSize = 256;
|
|||||||
const int kTimelineAssetLoadOppositeSize = 64;
|
const int kTimelineAssetLoadOppositeSize = 64;
|
||||||
|
|
||||||
// Widget keys
|
// Widget keys
|
||||||
|
const String appShareGroupId = "group.app.immich.share";
|
||||||
const String kWidgetAuthToken = "widget_auth_token";
|
const String kWidgetAuthToken = "widget_auth_token";
|
||||||
const String kWidgetServerEndpoint = "widget_server_url";
|
const String kWidgetServerEndpoint = "widget_server_url";
|
||||||
const String appShareGroupId = "group.app.immich.share";
|
const String kWidgetCustomHeaders = "widget_custom_headers";
|
||||||
|
|
||||||
// add widget identifiers here for new widgets
|
// add widget identifiers here for new widgets
|
||||||
// these are used to force a widget refresh
|
// these are used to force a widget refresh
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class HashService {
|
|||||||
if (hash?.length == 20) {
|
if (hash?.length == 20) {
|
||||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||||
} else {
|
} else {
|
||||||
_log.warning("Failed to hash file for ${asset.id}");
|
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +104,7 @@ class HashService {
|
|||||||
DLog.log("Hashed ${hashed.length}/${toHash.length} assets");
|
DLog.log("Hashed ${hashed.length}/${toHash.length} assets");
|
||||||
|
|
||||||
await _localAssetRepository.updateHashes(hashed);
|
await _localAssetRepository.updateHashes(hashed);
|
||||||
|
await _storageRepository.clearCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,18 @@ class RemoteAlbumService {
|
|||||||
return _repository.addUsers(albumId, userIds);
|
return _repository.addUsers(albumId, userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) async {
|
||||||
|
await _albumApiRepository.removeUser(albumId, userId: userId);
|
||||||
|
|
||||||
|
return _repository.removeUser(albumId, userId: userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setActivityStatus(String albumId, bool enabled) async {
|
||||||
|
await _albumApiRepository.setActivityStatus(albumId, enabled);
|
||||||
|
|
||||||
|
return _repository.setActivityStatus(albumId, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> getCount() {
|
Future<int> getCount() {
|
||||||
return _repository.getCount();
|
return _repository.getCount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,14 +139,18 @@ class SyncStreamService {
|
|||||||
return _syncStreamRepository.updateAlbumUsersV1(data.cast(), debugLabel: 'backfill');
|
return _syncStreamRepository.updateAlbumUsersV1(data.cast(), debugLabel: 'backfill');
|
||||||
case SyncEntityType.albumUserDeleteV1:
|
case SyncEntityType.albumUserDeleteV1:
|
||||||
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
||||||
case SyncEntityType.albumAssetV1:
|
case SyncEntityType.albumAssetCreateV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
|
||||||
|
case SyncEntityType.albumAssetUpdateV1:
|
||||||
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
|
||||||
case SyncEntityType.albumAssetBackfillV1:
|
case SyncEntityType.albumAssetBackfillV1:
|
||||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album backfill');
|
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
|
||||||
case SyncEntityType.albumAssetExifV1:
|
case SyncEntityType.albumAssetExifCreateV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album');
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
|
||||||
|
case SyncEntityType.albumAssetExifUpdateV1:
|
||||||
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif update');
|
||||||
case SyncEntityType.albumAssetExifBackfillV1:
|
case SyncEntityType.albumAssetExifBackfillV1:
|
||||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album backfill');
|
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif backfill');
|
||||||
case SyncEntityType.albumToAssetV1:
|
case SyncEntityType.albumToAssetV1:
|
||||||
return _syncStreamRepository.updateAlbumToAssetsV1(data.cast());
|
return _syncStreamRepository.updateAlbumToAssetsV1(data.cast());
|
||||||
case SyncEntityType.albumToAssetBackfillV1:
|
case SyncEntityType.albumToAssetBackfillV1:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class BackgroundSyncManager {
|
|||||||
this.onHashingError,
|
this.onHashingError,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> cancel() {
|
Future<void> cancel() async {
|
||||||
final futures = <Future>[];
|
final futures = <Future>[];
|
||||||
|
|
||||||
if (_syncTask != null) {
|
if (_syncTask != null) {
|
||||||
@@ -52,7 +52,11 @@ class BackgroundSyncManager {
|
|||||||
_syncWebsocketTask?.cancel();
|
_syncWebsocketTask?.cancel();
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
|
|
||||||
return Future.wait(futures);
|
try {
|
||||||
|
await Future.wait(futures);
|
||||||
|
} on CanceledError {
|
||||||
|
// Ignore cancellation errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to cancel the task, as it can also be run when the user logs out
|
// No need to cancel the task, as it can also be run when the user logs out
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ extension ContextHelper on BuildContext {
|
|||||||
// Returns the current height from MediaQuery
|
// Returns the current height from MediaQuery
|
||||||
double get height => MediaQuery.sizeOf(this).height;
|
double get height => MediaQuery.sizeOf(this).height;
|
||||||
|
|
||||||
|
// Returns the current size from MediaQuery
|
||||||
|
Size get sizeData => MediaQuery.sizeOf(this);
|
||||||
|
|
||||||
// Returns true if the app is running on a mobile device (!tablets)
|
// Returns true if the app is running on a mobile device (!tablets)
|
||||||
bool get isMobile => width < 550;
|
bool get isMobile => width < 550;
|
||||||
|
|
||||||
|
|||||||
10
mobile/lib/extensions/codec_extensions.dart
Normal file
10
mobile/lib/extensions/codec_extensions.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
extension CodecImageInfoExtension on Codec {
|
||||||
|
Future<ImageInfo> getImageInfo({double scale = 1.0}) async {
|
||||||
|
final frame = await getNextFrame();
|
||||||
|
return ImageInfo(image: frame.image, scale: scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,6 +113,7 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
final query = _db.localAssetEntity.select()
|
final query = _db.localAssetEntity.select()
|
||||||
..where(
|
..where(
|
||||||
(lae) =>
|
(lae) =>
|
||||||
|
lae.checksum.isNotNull() &
|
||||||
existsQuery(
|
existsQuery(
|
||||||
_db.localAlbumAssetEntity.selectOnly()
|
_db.localAlbumAssetEntity.selectOnly()
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
@@ -125,9 +126,7 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
|||||||
_db.remoteAssetEntity.selectOnly()
|
_db.remoteAssetEntity.selectOnly()
|
||||||
..addColumns([_db.remoteAssetEntity.checksum])
|
..addColumns([_db.remoteAssetEntity.checksum])
|
||||||
..where(
|
..where(
|
||||||
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) &
|
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & _db.remoteAssetEntity.ownerId.equals(userId),
|
||||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
||||||
lae.checksum.isNotNull(),
|
|
||||||
),
|
),
|
||||||
) &
|
) &
|
||||||
lae.id.isNotInQuery(_getExcludedSubquery()),
|
lae.id.isNotInQuery(_getExcludedSubquery()),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository {
|
|||||||
leftOuterJoin(_db.assetFaceEntity, _db.assetFaceEntity.personId.equalsExp(_db.personEntity.id)),
|
leftOuterJoin(_db.assetFaceEntity, _db.assetFaceEntity.personId.equalsExp(_db.personEntity.id)),
|
||||||
])
|
])
|
||||||
..where(_db.personEntity.isHidden.equals(false))
|
..where(_db.personEntity.isHidden.equals(false))
|
||||||
..groupBy([_db.personEntity.id])
|
..groupBy([_db.personEntity.id], having: _db.assetFaceEntity.id.count().isBiggerOrEqualValue(3))
|
||||||
..orderBy([
|
..orderBy([
|
||||||
OrderingTerm(expression: _db.personEntity.name.equals('').not(), mode: OrderingMode.desc),
|
OrderingTerm(expression: _db.personEntity.name.equals('').not(), mode: OrderingMode.desc),
|
||||||
OrderingTerm(expression: _db.assetFaceEntity.id.count(), mode: OrderingMode.desc),
|
OrderingTerm(expression: _db.assetFaceEntity.id.count(), mode: OrderingMode.desc),
|
||||||
|
|||||||
@@ -220,12 +220,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) {
|
||||||
|
return _db.remoteAlbumUserEntity.deleteWhere((row) => row.albumId.equals(albumId) & row.userId.equals(userId));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteAlbum(String albumId) async {
|
Future<void> deleteAlbum(String albumId) async {
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
await _db.remoteAlbumEntity.deleteWhere((table) => table.id.equals(albumId));
|
await _db.remoteAlbumEntity.deleteWhere((table) => table.id.equals(albumId));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setActivityStatus(String albumId, bool isEnabled) async {
|
||||||
|
final query = _db.update(_db.remoteAlbumEntity)..where((row) => row.id.equals(albumId));
|
||||||
|
|
||||||
|
await query.write(RemoteAlbumEntityCompanion(isActivityEnabled: Value(isEnabled)));
|
||||||
|
}
|
||||||
|
|
||||||
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
Stream<RemoteAlbum?> watchAlbum(String albumId) {
|
||||||
final query =
|
final query =
|
||||||
_db.remoteAlbumEntity.select().join([
|
_db.remoteAlbumEntity.select().join([
|
||||||
|
|||||||
@@ -186,6 +186,23 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteExifEntity,
|
||||||
|
RemoteExifEntityCompanion(dateTimeOriginal: Value(dateTime)),
|
||||||
|
where: (e) => e.assetId.equals(id),
|
||||||
|
);
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(createdAt: Value(dateTime)),
|
||||||
|
where: (e) => e.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> stack(String userId, StackResponse stack) {
|
Future<void> stack(String userId, StackResponse stack) {
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
final stackIds = await _db.managers.stackEntity
|
final stackIds = await _db.managers.stackEntity
|
||||||
|
|||||||
@@ -66,4 +66,14 @@ class StorageRepository {
|
|||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> clearCache() async {
|
||||||
|
final log = Logger('StorageRepository');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PhotoManager.clearFileCache();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
log.warning("Error clearing cache", error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson,
|
||||||
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
||||||
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
||||||
SyncEntityType.albumAssetV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson,
|
||||||
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
||||||
SyncEntityType.albumAssetExifV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson,
|
||||||
|
SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||||
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
|
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
|
||||||
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||||
@@ -83,7 +83,6 @@ Future<void> initApp() async {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PlatformDispatcher.instance.onError = (error, stack) {
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
debugPrint("FlutterError - Catch all: $error \n $stack");
|
|
||||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.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/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
@@ -14,7 +13,6 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
|||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
||||||
@@ -67,14 +65,14 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
||||||
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
||||||
|
|
||||||
handleSyncAlbumToggle(bool isEnable) async {
|
// handleSyncAlbumToggle(bool isEnable) async {
|
||||||
if (isEnable) {
|
// if (isEnable) {
|
||||||
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
// await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
for (final album in selectedBackupAlbums) {
|
// for (final album in selectedBackupAlbums) {
|
||||||
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
// await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
@@ -167,16 +165,15 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SettingsSwitchListTile(
|
// SettingsSwitchListTile(
|
||||||
valueNotifier: _enableSyncUploadAlbum,
|
// valueNotifier: _enableSyncUploadAlbum,
|
||||||
title: "sync_albums".t(context: context),
|
// title: "sync_albums".t(context: context),
|
||||||
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
// subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
// titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
// subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
||||||
onChanged: handleSyncAlbumToggle,
|
// onChanged: handleSyncAlbumToggle,
|
||||||
),
|
// ),
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -137,25 +139,50 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
|||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BottomNavigationBar extends ConsumerWidget {
|
class _BottomNavigationBar extends ConsumerStatefulWidget {
|
||||||
const _BottomNavigationBar({required this.tabsRouter, required this.destinations});
|
const _BottomNavigationBar({required this.tabsRouter, required this.destinations});
|
||||||
|
|
||||||
final List<Widget> destinations;
|
final List<Widget> destinations;
|
||||||
final TabsRouter tabsRouter;
|
final TabsRouter tabsRouter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState createState() => _BottomNavigationBarState();
|
||||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
}
|
||||||
final isMultiselectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
|
||||||
|
|
||||||
if (isScreenLandscape || isMultiselectEnabled) {
|
class _BottomNavigationBarState extends ConsumerState<_BottomNavigationBar> {
|
||||||
|
bool hideNavigationBar = false;
|
||||||
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_eventSubscription = EventStream.shared.listen<MultiSelectToggleEvent>(_onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEvent(MultiSelectToggleEvent event) {
|
||||||
|
setState(() {
|
||||||
|
hideNavigationBar = event.isEnabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_eventSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
|
||||||
|
if (isScreenLandscape || hideNavigationBar) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return NavigationBar(
|
return NavigationBar(
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: widget.tabsRouter.activeIndex,
|
||||||
onDestinationSelected: (index) => _onNavigationSelected(tabsRouter, index, ref),
|
onDestinationSelected: (index) => _onNavigationSelected(widget.tabsRouter, index, ref),
|
||||||
destinations: destinations,
|
destinations: widget.destinations,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,11 +264,15 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||||
);
|
);
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
|
||||||
|
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||||
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||||
|
|
||||||
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||||
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
||||||
serverUrl += '/';
|
serverUrl += '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLink != null && serverUrl != null) {
|
if (newLink != null && serverUrl != null) {
|
||||||
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
||||||
copyLinkToClipboard();
|
copyLinkToClipboard();
|
||||||
|
|||||||
2
mobile/lib/platform/native_sync_api.g.dart
generated
2
mobile/lib/platform/native_sync_api.g.dart
generated
@@ -1,4 +1,4 @@
|
|||||||
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
// See also: https://pub.dev/packages/pigeon
|
// See also: https://pub.dev/packages/pigeon
|
||||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MainTimelinePage extends ConsumerWidget {
|
class MainTimelinePage extends ConsumerWidget {
|
||||||
@@ -12,21 +13,24 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||||
|
final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true));
|
||||||
|
|
||||||
|
// TODO: the user preferences need to be updated
|
||||||
|
// from the server to get live hiding/showing of memory lane
|
||||||
|
|
||||||
return memoryLaneProvider.maybeWhen(
|
return memoryLaneProvider.maybeWhen(
|
||||||
data: (memories) {
|
data: (memories) {
|
||||||
return memories.isEmpty
|
return memories.isEmpty || !memoriesEnabled
|
||||||
? const Timeline(showStorageIndicator: true)
|
? const Timeline()
|
||||||
: Timeline(
|
: Timeline(
|
||||||
topSliverWidget: SliverToBoxAdapter(
|
topSliverWidget: SliverToBoxAdapter(
|
||||||
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
||||||
child: DriftMemoryLane(memories: memories),
|
child: DriftMemoryLane(memories: memories),
|
||||||
),
|
),
|
||||||
topSliverWidgetHeight: 200,
|
topSliverWidgetHeight: 200,
|
||||||
showStorageIndicator: true,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
orElse: () => const Timeline(showStorageIndicator: true),
|
orElse: () => const Timeline(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
237
mobile/lib/presentation/pages/drift_album_options.page.dart
Normal file
237
mobile/lib/presentation/pages/drift_album_options.page.dart
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftAlbumOptionsPage extends HookConsumerWidget {
|
||||||
|
const DriftAlbumOptionsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final album = ref.watch(currentRemoteAlbumProvider);
|
||||||
|
if (album == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final sharedUsersAsync = ref.watch(remoteAlbumSharedUsersProvider(album.id));
|
||||||
|
final userId = ref.watch(authProvider).userId;
|
||||||
|
final activityEnabled = useState(album.isActivityEnabled);
|
||||||
|
final isOwner = album.ownerId == userId;
|
||||||
|
|
||||||
|
void showErrorMessage() {
|
||||||
|
context.pop();
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "shared_album_section_people_action_error".t(context: context),
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void leaveAlbum() async {
|
||||||
|
try {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId);
|
||||||
|
context.navigateTo(const DriftAlbumsRoute());
|
||||||
|
} catch (_) {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeUserFromAlbum(UserDto user) async {
|
||||||
|
try {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).removeUser(album.id, user.id);
|
||||||
|
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
|
||||||
|
} catch (_) {
|
||||||
|
showErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addUsers() async {
|
||||||
|
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: album));
|
||||||
|
|
||||||
|
if (newUsers == null || newUsers.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers);
|
||||||
|
|
||||||
|
if (newUsers.isNotEmpty) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}),
|
||||||
|
toastType: ToastType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.invalidate(remoteAlbumSharedUsersProvider(album.id));
|
||||||
|
} catch (e) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Failed to add users to album: ${e.toString()}",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleUserClick(UserDto user) {
|
||||||
|
var actions = [];
|
||||||
|
|
||||||
|
if (user.id == userId) {
|
||||||
|
actions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.exit_to_app_rounded),
|
||||||
|
title: const Text("leave_album").t(context: context),
|
||||||
|
onTap: leaveAlbum,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOwner) {
|
||||||
|
actions = [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
|
title: const Text("remove_user").t(context: context),
|
||||||
|
onTap: () => removeUserFromAlbum(user),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainer,
|
||||||
|
isScrollControlled: false,
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOwnerInfo() {
|
||||||
|
if (isOwner) {
|
||||||
|
final owner = ref.watch(currentUserProvider);
|
||||||
|
return ListTile(
|
||||||
|
leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
|
||||||
|
title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(owner?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final usersProvider = ref.watch(driftUsersProvider);
|
||||||
|
return usersProvider.maybeWhen(
|
||||||
|
data: (users) {
|
||||||
|
final user = users.firstWhereOrNull((u) => u.id == album.ownerId);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: UserCircleAvatar(user: user, radius: 22),
|
||||||
|
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
orElse: () => const SizedBox(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSharedUsersList() {
|
||||||
|
return sharedUsersAsync.maybeWhen(
|
||||||
|
data: (sharedUsers) => ListView.builder(
|
||||||
|
primary: false,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: sharedUsers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = sharedUsers[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: UserCircleAvatar(user: user, radius: 22),
|
||||||
|
title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
|
subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)),
|
||||||
|
trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(),
|
||||||
|
onTap: userId == user.id || isOwner ? () => handleUserClick(user) : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
orElse: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSectionTitle(String text) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(text, style: context.textTheme.bodySmall),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
|
onPressed: () => context.maybePop(null),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text("options".t(context: context)),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (isOwner)
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: activityEnabled.value,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
activityEnabled.value = value;
|
||||||
|
await ref.read(remoteAlbumProvider.notifier).setActivityStatus(album.id, value);
|
||||||
|
},
|
||||||
|
activeColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"comments_and_likes",
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
).t(context: context),
|
||||||
|
subtitle: Text(
|
||||||
|
"let_others_respond",
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||||
|
).t(context: context),
|
||||||
|
),
|
||||||
|
buildSectionTitle("shared_album_section_people_title".t(context: context)),
|
||||||
|
if (isOwner) ...[
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_add_rounded),
|
||||||
|
title: Text("invite_people".t(context: context)),
|
||||||
|
onTap: () async => addUsers(),
|
||||||
|
),
|
||||||
|
const Divider(indent: 16),
|
||||||
|
],
|
||||||
|
buildOwnerInfo(),
|
||||||
|
buildSharedUsersList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,13 +28,15 @@ class RemoteAlbumPage extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||||
|
late RemoteAlbum _album;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_album = widget.album;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAssets(BuildContext context) async {
|
Future<void> addAssets(BuildContext context) async {
|
||||||
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(widget.album.id);
|
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(_album.id);
|
||||||
|
|
||||||
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
||||||
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
||||||
@@ -47,7 +49,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
final added = await ref
|
final added = await ref
|
||||||
.read(remoteAlbumProvider.notifier)
|
.read(remoteAlbumProvider.notifier)
|
||||||
.addAssets(
|
.addAssets(
|
||||||
widget.album.id,
|
_album.id,
|
||||||
newAssets.map((asset) {
|
newAssets.map((asset) {
|
||||||
final remoteAsset = asset as RemoteAsset;
|
final remoteAsset = asset as RemoteAsset;
|
||||||
return remoteAsset.id;
|
return remoteAsset.id;
|
||||||
@@ -64,14 +66,14 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addUsers(BuildContext context) async {
|
Future<void> addUsers(BuildContext context) async {
|
||||||
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: widget.album));
|
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: _album));
|
||||||
|
|
||||||
if (newUsers == null || newUsers.isEmpty) {
|
if (newUsers == null || newUsers.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref.read(remoteAlbumProvider.notifier).addUsers(widget.album.id, newUsers);
|
await ref.read(remoteAlbumProvider.notifier).addUsers(_album.id, newUsers);
|
||||||
|
|
||||||
if (newUsers.isNotEmpty) {
|
if (newUsers.isNotEmpty) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -81,7 +83,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.invalidate(remoteAlbumSharedUsersProvider(widget.album.id));
|
ref.invalidate(remoteAlbumSharedUsersProvider(_album.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -92,7 +94,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleAlbumOrder() async {
|
Future<void> toggleAlbumOrder() async {
|
||||||
await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder(widget.album.id);
|
await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder(_album.id);
|
||||||
|
|
||||||
ref.invalidate(timelineServiceProvider);
|
ref.invalidate(timelineServiceProvider);
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text('album_delete_confirmation'.t(context: context, args: {'album': widget.album.name})),
|
Text('album_delete_confirmation'.t(context: context, args: {'album': _album.name})),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('album_delete_confirmation_description'.t(context: context)),
|
Text('album_delete_confirmation_description'.t(context: context)),
|
||||||
],
|
],
|
||||||
@@ -128,7 +130,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(widget.album.id);
|
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(_album.id);
|
||||||
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -151,17 +153,20 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
final result = await showDialog<_EditAlbumData?>(
|
final result = await showDialog<_EditAlbumData?>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (context) => _EditAlbumDialog(album: widget.album),
|
builder: (context) => _EditAlbumDialog(album: _album),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null && context.mounted) {
|
if (result != null && context.mounted) {
|
||||||
|
setState(() {
|
||||||
|
_album = _album.copyWith(name: result.name, description: result.description ?? '');
|
||||||
|
});
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showOptionSheet(BuildContext context) {
|
void showOptionSheet(BuildContext context) {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isOwner = user != null ? user.id == widget.album.ownerId : false;
|
final isOwner = user != null ? user.id == _album.ownerId : false;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -195,6 +200,14 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
context.pop();
|
context.pop();
|
||||||
await showEditTitleAndDescription(context);
|
await showEditTitleAndDescription(context);
|
||||||
},
|
},
|
||||||
|
onCreateSharedLink: () async {
|
||||||
|
context.pop();
|
||||||
|
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
|
||||||
|
},
|
||||||
|
onShowOptions: () {
|
||||||
|
context.pop();
|
||||||
|
context.pushRoute(const DriftAlbumOptionsRoute());
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -205,7 +218,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: widget.album.id);
|
final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: _album.id);
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
@@ -217,7 +230,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
onToggleAlbumOrder: () => toggleAlbumOrder(),
|
onToggleAlbumOrder: () => toggleAlbumOrder(),
|
||||||
onEditTitle: () => showEditTitleAndDescription(context),
|
onEditTitle: () => showEditTitleAndDescription(context),
|
||||||
),
|
),
|
||||||
bottomSheet: RemoteAlbumBottomSheet(album: widget.album),
|
bottomSheet: RemoteAlbumBottomSheet(album: _album),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class DriftTrashPage extends StatelessWidget {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: Timeline(
|
||||||
showStorageIndicator: true,
|
|
||||||
appBar: SliverAppBar(
|
appBar: SliverAppBar(
|
||||||
title: Text('trash'.t(context: context)),
|
title: Text('trash'.t(context: context)),
|
||||||
floating: true,
|
floating: true,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class LocalTimelinePage extends StatelessWidget {
|
|||||||
child: Timeline(
|
child: Timeline(
|
||||||
appBar: MesmerizingSliverAppBar(title: album.name),
|
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||||
bottomSheet: const LocalAlbumBottomSheet(),
|
bottomSheet: const LocalAlbumBottomSheet(),
|
||||||
showStorageIndicator: true,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
@@ -627,7 +628,12 @@ class _SearchResultGrid extends ConsumerWidget {
|
|||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: Timeline(key: ValueKey(searchResult.totalAssets), appBar: null, groupBy: GroupAssetsBy.none),
|
child: Timeline(
|
||||||
|
key: ValueKey(searchResult.totalAssets),
|
||||||
|
groupBy: GroupAssetsBy.none,
|
||||||
|
appBar: null,
|
||||||
|
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class DownloadActionButton extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.download,
|
iconData: Icons.download,
|
||||||
|
maxWidth: 95,
|
||||||
label: "download".t(context: context),
|
label: "download".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,44 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/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/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class EditDateTimeActionButton extends ConsumerWidget {
|
class EditDateTimeActionButton extends ConsumerWidget {
|
||||||
const EditDateTimeActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const EditDateTimeActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
_onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).editDateTime(source, context);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'edit_date_and_time_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -12,6 +46,7 @@ class EditDateTimeActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 95.0,
|
maxWidth: 95.0,
|
||||||
iconData: Icons.edit_calendar_outlined,
|
iconData: Icons.edit_calendar_outlined,
|
||||||
label: "control_bottom_app_bar_edit_time".t(context: context),
|
label: "control_bottom_app_bar_edit_time".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
maxWidth: 100.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),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class RemoveFromAlbumActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.remove_circle_outline,
|
iconData: Icons.remove_circle_outline,
|
||||||
label: "remove_from_album".t(context: context),
|
label: "remove_from_album".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
maxWidth: 100,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
|
|||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.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/current_album.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/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -26,8 +27,9 @@ typedef AlbumSelectorCallback = void Function(RemoteAlbum album);
|
|||||||
|
|
||||||
class AlbumSelector extends ConsumerStatefulWidget {
|
class AlbumSelector extends ConsumerStatefulWidget {
|
||||||
final AlbumSelectorCallback onAlbumSelected;
|
final AlbumSelectorCallback onAlbumSelected;
|
||||||
|
final Function? onKeyboardExpanded;
|
||||||
|
|
||||||
const AlbumSelector({super.key, required this.onAlbumSelected});
|
const AlbumSelector({super.key, required this.onAlbumSelected, this.onKeyboardExpanded});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
|
ConsumerState<AlbumSelector> createState() => _AlbumSelectorState();
|
||||||
@@ -51,6 +53,12 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
|||||||
searchController.addListener(() {
|
searchController.addListener(() {
|
||||||
onSearch(searchController.text, filterMode);
|
onSearch(searchController.text, filterMode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
searchFocusNode.addListener(() {
|
||||||
|
if (searchFocusNode.hasFocus) {
|
||||||
|
widget.onKeyboardExpanded?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||||
@@ -578,6 +586,8 @@ class AddToAlbumHeader extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
context.pushRoute(RemoteAlbumRoute(album: newAlbum));
|
context.pushRoute(RemoteAlbumRoute(album: newAlbum));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,11 +147,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
// Precache both thumbnail and full image for smooth transitions
|
// Precache both thumbnail and full image for smooth transitions
|
||||||
unawaited(
|
unawaited(
|
||||||
Future.wait([
|
Future.wait([
|
||||||
precacheImage(
|
precacheImage(getThumbnailImageProvider(asset: asset), context, onError: (_, __) {}),
|
||||||
getThumbnailImageProvider(asset: asset, size: screenSize),
|
|
||||||
context,
|
|
||||||
onError: (_, __) {},
|
|
||||||
),
|
|
||||||
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
|
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@@ -482,7 +478,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
|
child: Thumbnail(asset: asset, fit: BoxFit.contain),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +509,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) {
|
PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) {
|
||||||
final size = Size(ctx.width, ctx.height);
|
final size = ctx.sizeData;
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
key: ValueKey(asset.heroTag),
|
key: ValueKey(asset.heroTag),
|
||||||
imageProvider: getFullImageProvider(asset, size: size),
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
@@ -529,10 +525,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
onTapDown: _onTapDown,
|
onTapDown: _onTapDown,
|
||||||
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder: (_, __, ___) => Container(
|
||||||
width: ctx.width,
|
width: size.width,
|
||||||
height: ctx.height,
|
height: size.height,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: size),
|
child: Thumbnail(asset: asset, fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -562,7 +558,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
image: Image(
|
image: Image(
|
||||||
key: ValueKey(asset),
|
key: ValueKey(asset),
|
||||||
image: getFullImageProvider(asset, size: Size(ctx.width, ctx.height)),
|
image: getFullImageProvider(asset, size: ctx.sizeData),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
height: ctx.height,
|
height: ctx.height,
|
||||||
width: ctx.width,
|
width: ctx.width,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
|||||||
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/download_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/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_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';
|
||||||
@@ -21,6 +22,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/she
|
|||||||
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/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.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/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
@@ -42,8 +44,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
@@ -61,6 +63,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
const DeleteLocalActionButton(source: ActionSource.viewer),
|
const DeleteLocalActionButton(source: ActionSource.viewer),
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
|
if (currentAlbum != null) RemoveFromAlbumActionButton(albumId: currentAlbum.id, source: ActionSource.viewer),
|
||||||
];
|
];
|
||||||
|
|
||||||
final lockedViewActions = <Widget>[];
|
final lockedViewActions = <Widget>[];
|
||||||
@@ -143,12 +146,18 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||||
|
|
||||||
|
Future<void> editDateTime() async {
|
||||||
|
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||||
|
}
|
||||||
|
|
||||||
return SliverList.list(
|
return SliverList.list(
|
||||||
children: [
|
children: [
|
||||||
// Asset Date and Time
|
// Asset Date and Time
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: _getDateTime(context, asset),
|
title: _getDateTime(context, asset),
|
||||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
|
||||||
|
onTap: asset.hasRemote ? () async => await editDateTime() : null,
|
||||||
),
|
),
|
||||||
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
|
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
|
||||||
const SheetPeopleDetails(),
|
const SheetPeopleDetails(),
|
||||||
@@ -194,11 +203,21 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
class _SheetTile extends StatelessWidget {
|
class _SheetTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
final TextStyle? titleStyle;
|
final TextStyle? titleStyle;
|
||||||
final TextStyle? subtitleStyle;
|
final TextStyle? subtitleStyle;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const _SheetTile({required this.title, this.titleStyle, this.leading, this.subtitle, this.subtitleStyle});
|
const _SheetTile({
|
||||||
|
required this.title,
|
||||||
|
this.titleStyle,
|
||||||
|
this.leading,
|
||||||
|
this.subtitle,
|
||||||
|
this.subtitleStyle,
|
||||||
|
this.trailing,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -234,8 +253,10 @@ class _SheetTile extends StatelessWidget {
|
|||||||
title: titleWidget,
|
title: titleWidget,
|
||||||
titleAlignment: ListTileTitleAlignment.center,
|
titleAlignment: ListTileTitleAlignment.center,
|
||||||
leading: leading,
|
leading: leading,
|
||||||
|
trailing: trailing,
|
||||||
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
|
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
|
||||||
subtitle: subtitleWidget,
|
subtitle: subtitleWidget,
|
||||||
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class _SheetPeopleDetailsState extends ConsumerState<SheetPeopleDetails> {
|
|||||||
context.back();
|
context.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.back();
|
context.pop();
|
||||||
context.pushRoute(DriftPersonRoute(person: person));
|
context.pushRoute(DriftPersonRoute(person: person));
|
||||||
},
|
},
|
||||||
onNameTap: () => showNameEditModal(person),
|
onNameTap: () => showNameEditModal(person),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
const StackActionButton(source: ActionSource.timeline),
|
const StackActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
|||||||
this.slivers,
|
this.slivers,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.initialChildSize = 0.35,
|
this.initialChildSize = 0.35,
|
||||||
this.minChildSize = 0.15,
|
double? minChildSize,
|
||||||
this.maxChildSize = 0.65,
|
this.maxChildSize = 0.65,
|
||||||
this.expand = true,
|
this.expand = true,
|
||||||
this.shouldCloseOnMinExtent = true,
|
this.shouldCloseOnMinExtent = true,
|
||||||
this.resizeOnScroll = true,
|
this.resizeOnScroll = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
}) : minChildSize = minChildSize ?? 0.15;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<BaseBottomSheet> createState() => _BaseDraggableScrollableSheetState();
|
ConsumerState<BaseBottomSheet> createState() => _BaseDraggableScrollableSheetState();
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
const StackActionButton(source: ActionSource.timeline),
|
const StackActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ 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/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/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_permanent_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/download_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/edit_date_time_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||||
@@ -25,11 +25,30 @@ 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';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class GeneralBottomSheet extends ConsumerWidget {
|
class GeneralBottomSheet extends ConsumerStatefulWidget {
|
||||||
const GeneralBottomSheet({super.key});
|
final double? minChildSize;
|
||||||
|
const GeneralBottomSheet({super.key, this.minChildSize});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<GeneralBottomSheet> createState() => _GeneralBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||||
|
late DraggableScrollableController sheetController;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
sheetController = DraggableScrollableController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
sheetController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final multiselect = ref.watch(multiSelectProvider);
|
final multiselect = ref.watch(multiSelectProvider);
|
||||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
|
||||||
@@ -58,8 +77,14 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onKeyboardExpand() {
|
||||||
|
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
|
controller: sheetController,
|
||||||
initialChildSize: 0.45,
|
initialChildSize: 0.45,
|
||||||
|
minChildSize: widget.minChildSize,
|
||||||
maxChildSize: 0.85,
|
maxChildSize: 0.85,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -69,26 +94,21 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(source: ActionSource.timeline),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
|
const StackActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
const DeleteActionButton(source: ActionSource.timeline),
|
const DeleteActionButton(source: ActionSource.timeline),
|
||||||
if (multiselect.hasLocal || multiselect.hasMerged) ...[
|
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
const EditDateTimeActionButton(),
|
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
|
||||||
const StackActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
|
||||||
if (multiselect.hasLocal) ...[
|
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
|
||||||
const UploadActionButton(source: ActionSource.timeline),
|
|
||||||
],
|
],
|
||||||
|
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
|
if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
slivers: [
|
slivers: [
|
||||||
const AddToAlbumHeader(),
|
const AddToAlbumHeader(),
|
||||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum),
|
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
|||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||||
const StackActionButton(source: ActionSource.timeline),
|
const StackActionButton(source: ActionSource.timeline),
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
|
|||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
|
||||||
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
||||||
// Create new provider and cache it
|
// Create new provider and cache it
|
||||||
final ImageProvider provider;
|
final ImageProvider provider;
|
||||||
if (_shouldUseLocalAsset(asset)) {
|
if (_shouldUseLocalAsset(asset)) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
provider = LocalFullImageProvider(id: id, name: asset.name, size: size, type: asset.type);
|
provider = LocalFullImageProvider(id: id, size: size, type: asset.type, updatedAt: asset.updatedAt);
|
||||||
} else {
|
} else {
|
||||||
final String assetId;
|
final String assetId;
|
||||||
if (asset is LocalAsset && asset.hasRemote) {
|
if (asset is LocalAsset && asset.hasRemote) {
|
||||||
@@ -26,7 +27,7 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
|
|||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = const Size.square(256)}) {
|
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = kThumbnailResolution}) {
|
||||||
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
||||||
|
|
||||||
if (remoteId != null) {
|
if (remoteId != null) {
|
||||||
@@ -35,7 +36,7 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
|||||||
|
|
||||||
if (_shouldUseLocalAsset(asset!)) {
|
if (_shouldUseLocalAsset(asset!)) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, name: asset.name, size: size);
|
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, size: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String assetId;
|
final String assetId;
|
||||||
@@ -52,3 +53,26 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
|||||||
|
|
||||||
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||||
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
||||||
|
|
||||||
|
ImageInfo? getCachedImage(ImageProvider key) {
|
||||||
|
ImageInfo? thumbnail;
|
||||||
|
final ImageStreamCompleter? stream = PaintingBinding.instance.imageCache.putIfAbsent(
|
||||||
|
key,
|
||||||
|
() => throw Exception(), // don't bother loading if it isn't cached
|
||||||
|
onError: (_, __) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stream != null) {
|
||||||
|
void listener(ImageInfo info, bool synchronousCall) {
|
||||||
|
thumbnail = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream.addListener(ImageStreamListener(listener));
|
||||||
|
} finally {
|
||||||
|
stream.removeListener(ImageStreamListener(listener));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.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/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/codec_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.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/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||||
@@ -22,14 +24,12 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final String name;
|
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
const LocalThumbProvider({
|
const LocalThumbProvider({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.name,
|
this.size = kThumbnailResolution,
|
||||||
this.size = const Size.square(kTimelineFixedTileExtent),
|
|
||||||
this.cacheManager,
|
this.cacheManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,10 +45,8 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
codec: _codec(key, cache, decode),
|
codec: _codec(key, cache, decode),
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
|
||||||
DiagnosticsProperty<String>('Id', key.id),
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||||
DiagnosticsProperty<String>('Name', key.name),
|
|
||||||
DiagnosticsProperty<Size>('Size', key.size),
|
DiagnosticsProperty<Size>('Size', key.size),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -68,7 +66,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||||
if (thumbnailBytes == null) {
|
if (thumbnailBytes == null) {
|
||||||
PaintingBinding.instance.imageCache.evict(key);
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
throw StateError("Loading thumb for local photo ${key.name} failed");
|
throw StateError("Loading thumb for local photo ${key.id} failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||||
@@ -94,11 +92,11 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final StorageRepository _storageRepository = const StorageRepository();
|
final StorageRepository _storageRepository = const StorageRepository();
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
|
||||||
final Size size;
|
final Size size;
|
||||||
final AssetType type;
|
final AssetType type;
|
||||||
|
final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed
|
||||||
|
|
||||||
const LocalFullImageProvider({required this.id, required this.name, required this.size, required this.type});
|
const LocalFullImageProvider({required this.id, required this.size, required this.type, required this.updatedAt});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@@ -107,52 +105,45 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
return MultiImageStreamCompleter(
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
codec: _codec(key, decode),
|
_codec(key, decode),
|
||||||
scale: 1.0,
|
initialImage: getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)),
|
||||||
informationCollector: () sync* {
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
yield ErrorDescription(name);
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
},
|
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||||
|
DiagnosticsProperty<Size>('Size', key.size),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streams in each stage of the image as we ask for it
|
// Streams in each stage of the image as we ask for it
|
||||||
Stream<Codec> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
try {
|
try {
|
||||||
switch (key.type) {
|
return switch (key.type) {
|
||||||
case AssetType.image:
|
AssetType.image => _decodeProgressive(key, decode),
|
||||||
yield* _decodeProgressive(key, decode);
|
AssetType.video => _getThumbnailCodec(key, decode),
|
||||||
break;
|
_ => throw StateError('Unsupported asset type ${key.type}'),
|
||||||
case AssetType.video:
|
};
|
||||||
final codec = await _getThumbnailCodec(key, decode);
|
|
||||||
if (codec == null) {
|
|
||||||
throw StateError("Failed to load preview for ${key.name}");
|
|
||||||
}
|
|
||||||
yield codec;
|
|
||||||
break;
|
|
||||||
case AssetType.other:
|
|
||||||
case AssetType.audio:
|
|
||||||
throw StateError('Unsupported asset type ${key.type}');
|
|
||||||
}
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.name}', error, stack);
|
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.id}', error, stack);
|
||||||
throw const ImageLoadingException('Could not load image from local storage');
|
throw const ImageLoadingException('Could not load image from local storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Codec?> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async {
|
Stream<ImageInfo> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||||
if (thumbBytes == null) {
|
if (thumbBytes == null) {
|
||||||
return null;
|
throw StateError("Failed to load preview for ${key.id}");
|
||||||
}
|
}
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
||||||
return decode(buffer);
|
final codec = await decode(buffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final file = await _storageRepository.getFileForAsset(key.id);
|
final file = await _storageRepository.getFileForAsset(key.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw StateError("Opening file for asset ${key.name} failed");
|
throw StateError("Opening file for asset ${key.id} failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final fileSize = await file.length();
|
final fileSize = await file.length();
|
||||||
@@ -171,7 +162,8 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||||
if (mediumThumb != null) {
|
if (mediumThumb != null) {
|
||||||
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
||||||
yield await decode(mediumBuffer);
|
final codec = await decode(mediumBuffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -187,24 +179,26 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||||
if (highThumb != null) {
|
if (highThumb != null) {
|
||||||
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
||||||
yield await decode(highBuffer);
|
final codec = await decode(highBuffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
||||||
yield await decode(buffer);
|
final codec = await decode(buffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
if (other is LocalFullImageProvider) {
|
if (other is LocalFullImageProvider) {
|
||||||
return id == other.id && size == other.size && type == other.type && name == other.name;
|
return id == other.id && size == other.size && type == other.type;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode;
|
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// The below code is adapted from cached_network_image package's
|
||||||
|
// MultiImageStreamCompleter to better suit one-frame image loading.
|
||||||
|
// In particular, it allows providing an initial image to emit synchronously.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
/// An ImageStreamCompleter with support for loading multiple images.
|
||||||
|
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||||
|
ImageInfo? _initialImage;
|
||||||
|
|
||||||
|
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images]
|
||||||
|
/// should be the primary images to display (typically asynchronously as they load).
|
||||||
|
/// The [initialImage] is an optional image that will be emitted synchronously
|
||||||
|
/// until the first stream image is completed, useful as a thumbnail or placeholder.
|
||||||
|
OneFramePlaceholderImageStreamCompleter(
|
||||||
|
Stream<ImageInfo> images, {
|
||||||
|
ImageInfo? initialImage,
|
||||||
|
InformationCollector? informationCollector,
|
||||||
|
}) {
|
||||||
|
_initialImage = initialImage;
|
||||||
|
images.listen(
|
||||||
|
_onImage,
|
||||||
|
onError: (Object error, StackTrace stack) {
|
||||||
|
reportError(
|
||||||
|
context: ErrorDescription('resolving a single-frame image stream'),
|
||||||
|
exception: error,
|
||||||
|
stack: stack,
|
||||||
|
informationCollector: informationCollector,
|
||||||
|
silent: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onImage(ImageInfo image) {
|
||||||
|
setImage(image);
|
||||||
|
_initialImage?.dispose();
|
||||||
|
_initialImage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addListener(ImageStreamListener listener) {
|
||||||
|
final initialImage = _initialImage;
|
||||||
|
if (initialImage != null) {
|
||||||
|
try {
|
||||||
|
listener.onImage(initialImage.clone(), true);
|
||||||
|
} catch (exception, stack) {
|
||||||
|
reportError(
|
||||||
|
context: ErrorDescription('by a synchronously-called image listener'),
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDisposed() {
|
||||||
|
_initialImage?.dispose();
|
||||||
|
_initialImage = null;
|
||||||
|
super.onDisposed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/codec_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
@@ -81,36 +83,28 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
|||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
return MultiImageStreamCompleter(
|
_codec(key, cache, decode),
|
||||||
codec: _codec(key, cache, decode, chunkEvents),
|
initialImage: getCachedImage(RemoteThumbProvider(assetId: key.assetId)),
|
||||||
scale: 1.0,
|
|
||||||
chunkEvents: chunkEvents.stream,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _codec(
|
Stream<ImageInfo> _codec(RemoteFullImageProvider key, CacheManager cache, ImageDecoderCallback decode) async* {
|
||||||
RemoteFullImageProvider key,
|
final codec = await ImageLoader.loadImageFromCache(
|
||||||
CacheManager cache,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
StreamController<ImageChunkEvent> chunkController,
|
|
||||||
) async* {
|
|
||||||
yield await ImageLoader.loadImageFromCache(
|
|
||||||
getPreviewUrlForRemoteId(key.assetId),
|
getPreviewUrlForRemoteId(key.assetId),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
decode: decode,
|
decode: decode,
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
|
|
||||||
if (AppSetting.get(Setting.loadOriginal)) {
|
if (AppSetting.get(Setting.loadOriginal)) {
|
||||||
yield await ImageLoader.loadImageFromCache(
|
final codec = await ImageLoader.loadImageFromCache(
|
||||||
getOriginalUrlForRemoteId(key.assetId),
|
getOriginalUrlForRemoteId(key.assetId),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
decode: decode,
|
decode: decode,
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
await chunkController.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Thumbnail extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||||
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size);
|
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId);
|
||||||
|
|
||||||
return OctoImage.fromSet(
|
return OctoImage.fromSet(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/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/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/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.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/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class ThumbnailTile extends ConsumerWidget {
|
class ThumbnailTile extends ConsumerWidget {
|
||||||
@@ -13,7 +15,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
this.asset, {
|
this.asset, {
|
||||||
this.size = const Size.square(256),
|
this.size = const Size.square(256),
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator,
|
||||||
this.lockSelection = false,
|
this.lockSelection = false,
|
||||||
this.heroOffset,
|
this.heroOffset,
|
||||||
super.key,
|
super.key,
|
||||||
@@ -22,7 +24,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
final Size size;
|
final Size size;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final bool lockSelection;
|
final bool lockSelection;
|
||||||
final int? heroOffset;
|
final int? heroOffset;
|
||||||
|
|
||||||
@@ -52,6 +54,9 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
|
|
||||||
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
||||||
|
|
||||||
|
final bool storageIndicator =
|
||||||
|
showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
@@ -86,7 +91,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
child: _VideoIndicator(asset.duration),
|
child: _VideoIndicator(asset.duration),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showStorageIndicator)
|
if (storageIndicator)
|
||||||
switch (asset.storage) {
|
switch (asset.storage) {
|
||||||
AssetState.local => const Align(
|
AssetState.local => const Align(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
this.onCreateSharedLink,
|
this.onCreateSharedLink,
|
||||||
this.onToggleAlbumOrder,
|
this.onToggleAlbumOrder,
|
||||||
this.onEditAlbum,
|
this.onEditAlbum,
|
||||||
|
this.onShowOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback? onAddPhotos;
|
final VoidCallback? onAddPhotos;
|
||||||
@@ -22,6 +23,7 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
final VoidCallback? onCreateSharedLink;
|
final VoidCallback? onCreateSharedLink;
|
||||||
final VoidCallback? onToggleAlbumOrder;
|
final VoidCallback? onToggleAlbumOrder;
|
||||||
final VoidCallback? onEditAlbum;
|
final VoidCallback? onEditAlbum;
|
||||||
|
final VoidCallback? onShowOptions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -69,6 +71,12 @@ class DriftRemoteAlbumOption extends ConsumerWidget {
|
|||||||
title: Text('create_shared_link'.t(context: context), style: textStyle),
|
title: Text('create_shared_link'.t(context: context), style: textStyle),
|
||||||
onTap: onCreateSharedLink,
|
onTap: onCreateSharedLink,
|
||||||
),
|
),
|
||||||
|
if (onShowOptions != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: Text('options'.t(context: context), style: textStyle),
|
||||||
|
onTap: onShowOptions,
|
||||||
|
),
|
||||||
if (onDeleteAlbum != null) ...[
|
if (onDeleteAlbum != null) ...[
|
||||||
const Divider(indent: 16, endIndent: 16),
|
const Divider(indent: 16, endIndent: 16),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
const double kTimelineHeaderExtent = 80.0;
|
const double kTimelineHeaderExtent = 80.0;
|
||||||
const double kTimelineFixedTileExtent = 256;
|
const Size kTimelineFixedTileExtent = Size.square(256);
|
||||||
|
const Size kThumbnailResolution = kTimelineFixedTileExtent;
|
||||||
const double kTimelineSpacing = 2.0;
|
const double kTimelineSpacing = 2.0;
|
||||||
const int kTimelineColumnCount = 3;
|
const int kTimelineColumnCount = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
|
||||||
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';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@@ -125,10 +126,14 @@ class _FixedSegmentRow extends ConsumerWidget {
|
|||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < assets.length; i++)
|
for (int i = 0; i < assets.length; i++)
|
||||||
_AssetTileWidget(
|
TimelineAssetIndexWrapper(
|
||||||
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
|
|
||||||
asset: assets[i],
|
|
||||||
assetIndex: assetIndex + i,
|
assetIndex: assetIndex + i,
|
||||||
|
segmentIndex: 0, // For simplicity, using 0 for now
|
||||||
|
child: _AssetTileWidget(
|
||||||
|
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
|
||||||
|
asset: assets[i],
|
||||||
|
assetIndex: assetIndex + i,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ abstract class SegmentBuilder {
|
|||||||
static Widget buildPlaceholder(
|
static Widget buildPlaceholder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
int count, {
|
int count, {
|
||||||
Size size = const Size.square(kTimelineFixedTileExtent),
|
Size size = kTimelineFixedTileExtent,
|
||||||
double spacing = kTimelineSpacing,
|
double spacing = kTimelineSpacing,
|
||||||
}) => RepaintBoundary(
|
}) => RepaintBoundary(
|
||||||
child: FixedTimelineRow(
|
child: FixedTimelineRow(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TimelineArgs {
|
|||||||
final double maxHeight;
|
final double maxHeight;
|
||||||
final double spacing;
|
final double spacing;
|
||||||
final int columnCount;
|
final int columnCount;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final bool withStack;
|
final bool withStack;
|
||||||
final GroupAssetsBy? groupBy;
|
final GroupAssetsBy? groupBy;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class TimelineArgs {
|
|||||||
required this.maxHeight,
|
required this.maxHeight,
|
||||||
this.spacing = kTimelineSpacing,
|
this.spacing = kTimelineSpacing,
|
||||||
this.columnCount = kTimelineColumnCount,
|
this.columnCount = kTimelineColumnCount,
|
||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator,
|
||||||
this.withStack = false,
|
this.withStack = false,
|
||||||
this.groupBy,
|
this.groupBy,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
@@ -15,6 +18,7 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_s
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
@@ -27,7 +31,7 @@ class Timeline extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator,
|
||||||
this.withStack = false,
|
this.withStack = false,
|
||||||
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
||||||
this.bottomSheet = const GeneralBottomSheet(),
|
this.bottomSheet = const GeneralBottomSheet(),
|
||||||
@@ -36,7 +40,7 @@ class Timeline extends StatelessWidget {
|
|||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
final double? topSliverWidgetHeight;
|
final double? topSliverWidgetHeight;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withStack;
|
final bool withStack;
|
||||||
@@ -88,10 +92,31 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
|
// Drag selection state
|
||||||
|
bool _dragging = false;
|
||||||
|
TimelineAssetIndex? _dragAnchorIndex;
|
||||||
|
final Set<BaseAsset> _draggedAssets = HashSet();
|
||||||
|
ScrollPhysics? _scrollPhysics;
|
||||||
|
|
||||||
|
int _perRow = 4;
|
||||||
|
double _scaleFactor = 3.0;
|
||||||
|
double _baseScaleFactor = 3.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
|
||||||
|
setState(() {
|
||||||
|
_perRow = currentTilesPerRow;
|
||||||
|
_scaleFactor = 7.0 - _perRow;
|
||||||
|
_baseScaleFactor = _scaleFactor;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
@@ -107,6 +132,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onMultiSelectionToggled(_, bool isEnabled) {
|
||||||
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -150,6 +179,71 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drag selection methods
|
||||||
|
void _setDragStartIndex(TimelineAssetIndex index) {
|
||||||
|
setState(() {
|
||||||
|
_scrollPhysics = const ClampingScrollPhysics();
|
||||||
|
_dragAnchorIndex = index;
|
||||||
|
_dragging = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopDrag() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Update the physics post frame to prevent sudden change in physics on iOS.
|
||||||
|
setState(() {
|
||||||
|
_scrollPhysics = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setState(() {
|
||||||
|
_dragging = false;
|
||||||
|
_draggedAssets.clear();
|
||||||
|
});
|
||||||
|
// Reset the scrolling state after a small delay to allow bottom sheet to expand again
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (mounted) {
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrolling(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _dragScroll(ScrollDirection direction) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.offset + (direction == ScrollDirection.forward ? 175 : -175),
|
||||||
|
duration: const Duration(milliseconds: 125),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragAssetEnter(TimelineAssetIndex index) {
|
||||||
|
if (_dragAnchorIndex == null || !_dragging) return;
|
||||||
|
|
||||||
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
final dragAnchorIndex = _dragAnchorIndex!;
|
||||||
|
|
||||||
|
// Calculate the range of assets to select
|
||||||
|
final startIndex = math.min(dragAnchorIndex.assetIndex, index.assetIndex);
|
||||||
|
final endIndex = math.max(dragAnchorIndex.assetIndex, index.assetIndex);
|
||||||
|
final count = endIndex - startIndex + 1;
|
||||||
|
|
||||||
|
// Load the assets in the range
|
||||||
|
if (timelineService.hasRange(startIndex, count)) {
|
||||||
|
final selectedAssets = timelineService.getAssets(startIndex, count);
|
||||||
|
|
||||||
|
// Clear previous drag selection and add new range
|
||||||
|
final multiSelectNotifier = ref.read(multiSelectProvider.notifier);
|
||||||
|
for (final asset in _draggedAssets) {
|
||||||
|
multiSelectNotifier.deselectAsset(asset);
|
||||||
|
}
|
||||||
|
_draggedAssets.clear();
|
||||||
|
|
||||||
|
for (final asset in selectedAssets) {
|
||||||
|
multiSelectNotifier.selectAsset(asset);
|
||||||
|
_draggedAssets.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext _) {
|
Widget build(BuildContext _) {
|
||||||
final asyncSegments = ref.watch(timelineSegmentProvider);
|
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||||
@@ -177,43 +271,83 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Stack(
|
child: RawGestureDetector(
|
||||||
children: [
|
gestures: {
|
||||||
Scrubber(
|
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
||||||
layoutSegments: segments,
|
() => CustomScaleGestureRecognizer(),
|
||||||
timelineHeight: maxHeight,
|
(CustomScaleGestureRecognizer scale) {
|
||||||
topPadding: topPadding,
|
scale.onStart = (details) {
|
||||||
bottomPadding: bottomPadding,
|
_baseScaleFactor = _scaleFactor;
|
||||||
monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
};
|
||||||
child: CustomScrollView(
|
|
||||||
primary: true,
|
scale.onUpdate = (details) {
|
||||||
cacheExtent: maxHeight * 2,
|
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
||||||
slivers: [
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!,
|
|
||||||
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
if (newPerRow != _perRow) {
|
||||||
_SliverSegmentedList(
|
setState(() {
|
||||||
segments: segments,
|
_scaleFactor = newScaleFactor;
|
||||||
delegate: SliverChildBuilderDelegate(
|
_perRow = newPerRow;
|
||||||
(ctx, index) {
|
});
|
||||||
if (index >= childCount) return null;
|
|
||||||
final segment = segments.findByIndex(index);
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
return segment?.builder(ctx, index) ?? const SizedBox.shrink();
|
}
|
||||||
},
|
};
|
||||||
childCount: childCount,
|
},
|
||||||
addAutomaticKeepAlives: false,
|
|
||||||
// We add repaint boundary around tiles, so skip the auto boundaries
|
|
||||||
addRepaintBoundaries: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
},
|
||||||
const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
|
child: TimelineDragRegion(
|
||||||
if (widget.bottomSheet != null) widget.bottomSheet!,
|
onStart: _setDragStartIndex,
|
||||||
],
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
],
|
onEnd: _stopDrag,
|
||||||
|
onScroll: _dragScroll,
|
||||||
|
onScrollStart: () {
|
||||||
|
// Minimize the bottom sheet when drag selection starts
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Scrubber(
|
||||||
|
layoutSegments: segments,
|
||||||
|
timelineHeight: maxHeight,
|
||||||
|
topPadding: topPadding,
|
||||||
|
bottomPadding: bottomPadding,
|
||||||
|
monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
||||||
|
child: CustomScrollView(
|
||||||
|
primary: true,
|
||||||
|
physics: _scrollPhysics,
|
||||||
|
cacheExtent: maxHeight * 2,
|
||||||
|
slivers: [
|
||||||
|
if (isSelectionMode)
|
||||||
|
const SelectionSliverAppBar()
|
||||||
|
else if (widget.appBar != null)
|
||||||
|
widget.appBar!,
|
||||||
|
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
||||||
|
_SliverSegmentedList(
|
||||||
|
segments: segments,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, index) {
|
||||||
|
if (index >= childCount) return null;
|
||||||
|
final segment = segments.findByIndex(index);
|
||||||
|
return segment?.builder(ctx, index) ?? const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
childCount: childCount,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
|
// We add repaint boundary around tiles, so skip the auto boundaries
|
||||||
|
addRepaintBoundaries: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
||||||
|
const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
|
||||||
|
if (widget.bottomSheet != null) widget.bottomSheet!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -443,3 +577,11 @@ class _MultiSelectStatusButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// accepts a gesture even though it should reject it (because child won)
|
||||||
|
class CustomScaleGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
|
@override
|
||||||
|
void rejectGesture(int pointer) {
|
||||||
|
acceptGesture(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class TimelineDragRegion extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
final void Function(TimelineAssetIndex valueKey)? onStart;
|
||||||
|
final void Function(TimelineAssetIndex valueKey)? onAssetEnter;
|
||||||
|
final void Function()? onEnd;
|
||||||
|
final void Function()? onScrollStart;
|
||||||
|
final void Function(ScrollDirection direction)? onScroll;
|
||||||
|
|
||||||
|
const TimelineDragRegion({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.onStart,
|
||||||
|
this.onAssetEnter,
|
||||||
|
this.onEnd,
|
||||||
|
this.onScrollStart,
|
||||||
|
this.onScroll,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _TimelineDragRegionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineDragRegionState extends State<TimelineDragRegion> {
|
||||||
|
late TimelineAssetIndex? assetUnderPointer;
|
||||||
|
late TimelineAssetIndex? anchorAsset;
|
||||||
|
|
||||||
|
// Scroll related state
|
||||||
|
static const double scrollOffset = 0.10;
|
||||||
|
double? topScrollOffset;
|
||||||
|
double? bottomScrollOffset;
|
||||||
|
Timer? scrollTimer;
|
||||||
|
late bool scrollNotified;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
assetUnderPointer = null;
|
||||||
|
anchorAsset = null;
|
||||||
|
scrollNotified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
topScrollOffset = null;
|
||||||
|
bottomScrollOffset = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
scrollTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RawGestureDetector(
|
||||||
|
gestures: {
|
||||||
|
_CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>(
|
||||||
|
() => _CustomLongPressGestureRecognizer(),
|
||||||
|
_registerCallbacks,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) {
|
||||||
|
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
|
||||||
|
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
|
||||||
|
recognizer.onLongPressUp = _onLongPressEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineAssetIndex? _getValueKeyAtPosition(Offset position) {
|
||||||
|
final box = context.findAncestorRenderObjectOfType<RenderBox>();
|
||||||
|
if (box == null) return null;
|
||||||
|
|
||||||
|
final hitTestResult = BoxHitTestResult();
|
||||||
|
final local = box.globalToLocal(position);
|
||||||
|
if (!box.hitTest(hitTestResult, position: local)) return null;
|
||||||
|
|
||||||
|
return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _TimelineAssetIndexProxy)?.target
|
||||||
|
as _TimelineAssetIndexProxy?)
|
||||||
|
?.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressStart(LongPressStartDetails event) {
|
||||||
|
/// Calculate widget height and scroll offset when long press starting instead of in [initState]
|
||||||
|
/// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size
|
||||||
|
final height = context.size?.height;
|
||||||
|
if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) {
|
||||||
|
topScrollOffset = height * scrollOffset;
|
||||||
|
bottomScrollOffset = height - topScrollOffset!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final initialHit = _getValueKeyAtPosition(event.globalPosition);
|
||||||
|
anchorAsset = initialHit;
|
||||||
|
if (initialHit == null) return;
|
||||||
|
|
||||||
|
if (anchorAsset != null) {
|
||||||
|
widget.onStart?.call(anchorAsset!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressEnd() {
|
||||||
|
scrollNotified = false;
|
||||||
|
scrollTimer?.cancel();
|
||||||
|
widget.onEnd?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressMove(LongPressMoveUpdateDetails event) {
|
||||||
|
if (anchorAsset == null) return;
|
||||||
|
if (topScrollOffset == null || bottomScrollOffset == null) return;
|
||||||
|
|
||||||
|
final currentDy = event.localPosition.dy;
|
||||||
|
|
||||||
|
if (currentDy > bottomScrollOffset!) {
|
||||||
|
scrollTimer ??= Timer.periodic(
|
||||||
|
const Duration(milliseconds: 50),
|
||||||
|
(_) => widget.onScroll?.call(ScrollDirection.forward),
|
||||||
|
);
|
||||||
|
} else if (currentDy < topScrollOffset!) {
|
||||||
|
scrollTimer ??= Timer.periodic(
|
||||||
|
const Duration(milliseconds: 50),
|
||||||
|
(_) => widget.onScroll?.call(ScrollDirection.reverse),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
scrollTimer?.cancel();
|
||||||
|
scrollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentlyTouchingAsset = _getValueKeyAtPosition(event.globalPosition);
|
||||||
|
if (currentlyTouchingAsset == null) return;
|
||||||
|
|
||||||
|
if (assetUnderPointer != currentlyTouchingAsset) {
|
||||||
|
if (!scrollNotified) {
|
||||||
|
scrollNotified = true;
|
||||||
|
widget.onScrollStart?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.onAssetEnter?.call(currentlyTouchingAsset);
|
||||||
|
assetUnderPointer = currentlyTouchingAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer {
|
||||||
|
@override
|
||||||
|
void rejectGesture(int pointer) {
|
||||||
|
acceptGesture(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineAssetIndexWrapper extends SingleChildRenderObjectWidget {
|
||||||
|
final int assetIndex;
|
||||||
|
final int segmentIndex;
|
||||||
|
|
||||||
|
const TimelineAssetIndexWrapper({
|
||||||
|
required Widget super.child,
|
||||||
|
required this.assetIndex,
|
||||||
|
required this.segmentIndex,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_TimelineAssetIndexProxy createRenderObject(BuildContext context) {
|
||||||
|
return _TimelineAssetIndexProxy(
|
||||||
|
index: TimelineAssetIndex(assetIndex: assetIndex, segmentIndex: segmentIndex),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_TimelineAssetIndexProxy renderObject,
|
||||||
|
) {
|
||||||
|
renderObject.index = TimelineAssetIndex(assetIndex: assetIndex, segmentIndex: segmentIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineAssetIndexProxy extends RenderProxyBox {
|
||||||
|
TimelineAssetIndex index;
|
||||||
|
|
||||||
|
_TimelineAssetIndexProxy({required this.index});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineAssetIndex {
|
||||||
|
final int assetIndex;
|
||||||
|
final int segmentIndex;
|
||||||
|
|
||||||
|
const TimelineAssetIndex({required this.assetIndex, required this.segmentIndex});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant TimelineAssetIndex other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.assetIndex == assetIndex && other.segmentIndex == segmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assetIndex.hashCode ^ segmentIndex.hashCode;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import 'package:immich_mobile/providers/tab.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
@@ -86,11 +86,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
// Ensure proper cleanup before starting new background tasks
|
// Ensure proper cleanup before starting new background tasks
|
||||||
try {
|
try {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
backgroundManager.syncLocal().then((_) {
|
Future(() async {
|
||||||
|
await backgroundManager.syncLocal();
|
||||||
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
||||||
// Check if app is still active before hashing
|
// Check if app is still active before hashing
|
||||||
if (state == AppLifeCycleEnum.resumed) {
|
if ([AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state)) {
|
||||||
backgroundManager.hashAssets();
|
await backgroundManager.hashAssets();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
backgroundManager.syncRemote(),
|
backgroundManager.syncRemote(),
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
Future<bool> saveAuthInfo({required String accessToken}) async {
|
Future<bool> saveAuthInfo({required String accessToken}) async {
|
||||||
await _apiService.setAccessToken(accessToken);
|
await _apiService.setAccessToken(accessToken);
|
||||||
|
|
||||||
await _widgetService.writeCredentials(Store.get(StoreKey.serverEndpoint), accessToken);
|
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
|
final customHeaders = Store.tryGet(StoreKey.customHeaders);
|
||||||
|
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
|
||||||
|
|
||||||
// Get the deviceid from the store if it exists, otherwise generate a new one
|
// Get the deviceid from the store if it exists, otherwise generate a new one
|
||||||
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/backup_album.service.dart';
|
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||||
import 'package:immich_mobile/services/server_info.service.dart';
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
|
|
||||||
class IOSBackgroundSettings {
|
class IOSBackgroundSettings {
|
||||||
final bool appRefreshEnabled;
|
final bool appRefreshEnabled;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/backup_album.service.dart';
|
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/models/download/livephotos_medatada.model.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/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/services/action.service.dart';
|
import 'package:immich_mobile/services/action.service.dart';
|
||||||
|
import 'package:immich_mobile/services/download.service.dart';
|
||||||
import 'package:immich_mobile/services/timeline.service.dart';
|
import 'package:immich_mobile/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/services/upload.service.dart';
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -30,6 +33,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
final Logger _logger = Logger('ActionNotifier');
|
final Logger _logger = Logger('ActionNotifier');
|
||||||
late ActionService _service;
|
late ActionService _service;
|
||||||
late UploadService _uploadService;
|
late UploadService _uploadService;
|
||||||
|
late DownloadService _downloadService;
|
||||||
|
|
||||||
ActionNotifier() : super();
|
ActionNotifier() : super();
|
||||||
|
|
||||||
@@ -37,6 +41,29 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
void build() {
|
void build() {
|
||||||
_uploadService = ref.watch(uploadServiceProvider);
|
_uploadService = ref.watch(uploadServiceProvider);
|
||||||
_service = ref.watch(actionServiceProvider);
|
_service = ref.watch(actionServiceProvider);
|
||||||
|
_downloadService = ref.watch(downloadServiceProvider);
|
||||||
|
_downloadService.onImageDownloadStatus = _downloadImageCallback;
|
||||||
|
_downloadService.onVideoDownloadStatus = _downloadVideoCallback;
|
||||||
|
_downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _downloadImageCallback(TaskStatusUpdate update) {
|
||||||
|
if (update.status == TaskStatus.complete) {
|
||||||
|
_downloadService.saveImageWithPath(update.task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _downloadVideoCallback(TaskStatusUpdate update) {
|
||||||
|
if (update.status == TaskStatus.complete) {
|
||||||
|
_downloadService.saveVideo(update.task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _downloadLivePhotoCallback(TaskStatusUpdate update) async {
|
||||||
|
if (update.status == TaskStatus.complete) {
|
||||||
|
final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id;
|
||||||
|
_downloadService.saveLivePhotos(update.task, livePhotosId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _getRemoteIdsForSource(ActionSource source) {
|
List<String> _getRemoteIdsForSource(ActionSource source) {
|
||||||
@@ -239,6 +266,21 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActionResult?> editDateTime(ActionSource source, BuildContext context) async {
|
||||||
|
final ids = _getOwnedRemoteIdsForSource(source);
|
||||||
|
try {
|
||||||
|
final isEdited = await _service.editDateTime(ids, context);
|
||||||
|
if (!isEdited) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to edit date and time for assets', error, stack);
|
||||||
|
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
|
Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
|
||||||
final ids = _getRemoteIdsForSource(source);
|
final ids = _getRemoteIdsForSource(source);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -156,6 +156,23 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
|||||||
Future<void> addUsers(String albumId, List<String> userIds) {
|
Future<void> addUsers(String albumId, List<String> userIds) {
|
||||||
return _remoteAlbumService.addUsers(albumId: albumId, userIds: userIds);
|
return _remoteAlbumService.addUsers(albumId: albumId, userIds: userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, String userId) {
|
||||||
|
return _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> leaveAlbum(String albumId, {required String userId}) async {
|
||||||
|
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||||
|
|
||||||
|
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||||
|
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
||||||
|
|
||||||
|
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setActivityStatus(String albumId, bool enabled) {
|
||||||
|
return _remoteAlbumService.setActivityStatus(albumId, enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteAlbumDateRangeProvider = FutureProvider.family<(DateTime, DateTime), String>((ref, albumId) async {
|
final remoteAlbumDateRangeProvider = FutureProvider.family<(DateTime, DateTime), String>((ref, albumId) async {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectState>(
|
final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectState>(
|
||||||
@@ -10,6 +10,11 @@ final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectSta
|
|||||||
dependencies: [timelineServiceProvider],
|
dependencies: [timelineServiceProvider],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class MultiSelectToggleEvent extends Event {
|
||||||
|
final bool isEnabled;
|
||||||
|
const MultiSelectToggleEvent(this.isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
class MultiSelectState {
|
class MultiSelectState {
|
||||||
final Set<BaseAsset> selectedAssets;
|
final Set<BaseAsset> selectedAssets;
|
||||||
final Set<BaseAsset> lockedSelectionAssets;
|
final Set<BaseAsset> lockedSelectionAssets;
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
||||||
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: dateTime.toIso8601String()));
|
||||||
|
}
|
||||||
|
|
||||||
Future<StackResponse> stack(List<String> ids) async {
|
Future<StackResponse> stack(List<String> ids) async {
|
||||||
final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
final responseDto = await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class AuthApiRepository extends ApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
|
if (_apiService.apiClient.basePath.isEmpty) return;
|
||||||
|
|
||||||
await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7));
|
await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ class DriftAlbumApiRepository extends ApiRepository {
|
|||||||
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
|
final response = await checkNull(_api.addUsersToAlbum(albumId, AddUsersDto(albumUsers: albumUsers)));
|
||||||
return response.toRemoteAlbum();
|
return response.toRemoteAlbum();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeUser(String albumId, {required String userId}) async {
|
||||||
|
await _api.removeUserFromAlbum(albumId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setActivityStatus(String albumId, bool isEnabled) async {
|
||||||
|
final response = await checkNull(_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: isEnabled)));
|
||||||
|
return response.isActivityEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AlbumResponseDto {
|
extension on AlbumResponseDto {
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'
|
|||||||
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
||||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/drift_backup.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart';
|
import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
|
import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
@@ -81,6 +81,7 @@ import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.da
|
|||||||
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/drift_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
|
||||||
@@ -95,9 +96,9 @@ import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
|
||||||
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart';
|
import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart';
|
||||||
@@ -329,6 +330,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
|
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
|
||||||
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
|
AutoRoute(page: DriftAlbumOptionsRoute.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: '/'),
|
||||||
|
|||||||
@@ -667,6 +667,22 @@ class CropImageRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftAlbumOptionsPage]
|
||||||
|
class DriftAlbumOptionsRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftAlbumOptionsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftAlbumOptionsRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftAlbumOptionsRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftAlbumOptionsPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftAlbumsPage]
|
/// [DriftAlbumsPage]
|
||||||
class DriftAlbumsRoute extends PageRouteInfo<void> {
|
class DriftAlbumsRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
|||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -159,6 +160,44 @@ class ActionService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> editDateTime(List<String> remoteIds, BuildContext context) async {
|
||||||
|
DateTime? initialDate;
|
||||||
|
String? timeZone;
|
||||||
|
Duration? offset;
|
||||||
|
|
||||||
|
if (remoteIds.length == 1) {
|
||||||
|
final assetId = remoteIds.first;
|
||||||
|
final asset = await _remoteAssetRepository.get(assetId);
|
||||||
|
if (asset == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final exifData = await _remoteAssetRepository.getExif(assetId);
|
||||||
|
initialDate = asset.createdAt.toLocal();
|
||||||
|
offset = initialDate.timeZoneOffset;
|
||||||
|
timeZone = exifData?.timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dateTime = await showDateTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialDateTime: initialDate,
|
||||||
|
initialTZ: timeZone,
|
||||||
|
initialTZOffset: offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dateTime == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert dateTime to DateTime object
|
||||||
|
final parsedDateTime = DateTime.parse(dateTime);
|
||||||
|
|
||||||
|
await _assetApiRepository.updateDateTime(remoteIds, parsedDateTime);
|
||||||
|
await _remoteAssetRepository.updateDateTime(remoteIds, parsedDateTime);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
||||||
int removedCount = 0;
|
int removedCount = 0;
|
||||||
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/backgroundLegacy.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
class HashService {
|
class HashService {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class UploadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> manualBackup(List<LocalAsset> localAssets) async {
|
Future<void> manualBackup(List<LocalAsset> localAssets) async {
|
||||||
|
await _storageRepository.clearCache();
|
||||||
List<UploadTask> tasks = [];
|
List<UploadTask> tasks = [];
|
||||||
for (final asset in localAssets) {
|
for (final asset in localAssets) {
|
||||||
final task = await _getUploadTask(
|
final task = await _getUploadTask(
|
||||||
@@ -120,6 +121,8 @@ class UploadService {
|
|||||||
/// Build the upload tasks
|
/// Build the upload tasks
|
||||||
/// Enqueue the tasks
|
/// Enqueue the tasks
|
||||||
Future<void> startBackup(String userId, void Function(EnqueueStatus status) onEnqueueTasks) async {
|
Future<void> startBackup(String userId, void Function(EnqueueStatus status) onEnqueueTasks) async {
|
||||||
|
await _storageRepository.clearCache();
|
||||||
|
|
||||||
shouldAbortQueuingTasks = false;
|
shouldAbortQueuingTasks = false;
|
||||||
|
|
||||||
final candidates = await _backupRepository.getCandidates(userId);
|
final candidates = await _backupRepository.getCandidates(userId);
|
||||||
@@ -159,6 +162,7 @@ class UploadService {
|
|||||||
Future<int> cancelBackup() async {
|
Future<int> cancelBackup() async {
|
||||||
shouldAbortQueuingTasks = true;
|
shouldAbortQueuingTasks = true;
|
||||||
|
|
||||||
|
await _storageRepository.clearCache();
|
||||||
await _uploadRepository.reset(kBackupGroup);
|
await _uploadRepository.reset(kBackupGroup);
|
||||||
await _uploadRepository.deleteDatabaseRecords(kBackupGroup);
|
await _uploadRepository.deleteDatabaseRecords(kBackupGroup);
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ class WidgetService {
|
|||||||
|
|
||||||
const WidgetService(this._repository);
|
const WidgetService(this._repository);
|
||||||
|
|
||||||
Future<void> writeCredentials(String serverURL, String sessionKey) async {
|
Future<void> writeCredentials(String serverURL, String sessionKey, String? customHeaders) async {
|
||||||
await _repository.setAppGroupId(appShareGroupId);
|
await _repository.setAppGroupId(appShareGroupId);
|
||||||
await _repository.saveData(kWidgetServerEndpoint, serverURL);
|
await _repository.saveData(kWidgetServerEndpoint, serverURL);
|
||||||
await _repository.saveData(kWidgetAuthToken, sessionKey);
|
await _repository.saveData(kWidgetAuthToken, sessionKey);
|
||||||
|
|
||||||
|
if (customHeaders != null && customHeaders.isNotEmpty) {
|
||||||
|
await _repository.saveData(kWidgetCustomHeaders, customHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
// wait 3 seconds to ensure the widget is updated, dont block
|
// wait 3 seconds to ensure the widget is updated, dont block
|
||||||
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
||||||
}
|
}
|
||||||
@@ -24,6 +28,7 @@ class WidgetService {
|
|||||||
await _repository.setAppGroupId(appShareGroupId);
|
await _repository.setAppGroupId(appShareGroupId);
|
||||||
await _repository.saveData(kWidgetServerEndpoint, "");
|
await _repository.saveData(kWidgetServerEndpoint, "");
|
||||||
await _repository.saveData(kWidgetAuthToken, "");
|
await _repository.saveData(kWidgetAuthToken, "");
|
||||||
|
await _repository.saveData(kWidgetCustomHeaders, "");
|
||||||
|
|
||||||
// wait 3 seconds to ensure the widget is updated, dont block
|
// wait 3 seconds to ensure the widget is updated, dont block
|
||||||
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
Future.delayed(const Duration(seconds: 3), refreshWidgets);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.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/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||||
|
|
||||||
class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
||||||
@@ -22,17 +24,20 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
|||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return GestureDetector(
|
||||||
height: 50,
|
onTap: () => context.pushRoute(const DriftAlbumOptionsRoute()),
|
||||||
child: ListView.builder(
|
child: SizedBox(
|
||||||
scrollDirection: Axis.horizontal,
|
height: 50,
|
||||||
itemBuilder: ((context, index) {
|
child: ListView.builder(
|
||||||
return Padding(
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
itemBuilder: ((context, index) {
|
||||||
child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true),
|
return Padding(
|
||||||
);
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
}),
|
child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true),
|
||||||
itemCount: sharedUsers.length,
|
);
|
||||||
|
}),
|
||||||
|
itemCount: sharedUsers.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class _DateTimePicker extends HookWidget {
|
|||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
trailing: Icon(Icons.edit_outlined, size: 18, color: context.primaryColor),
|
trailing: Icon(Icons.edit_outlined, size: 18, color: context.primaryColor),
|
||||||
title: Text(DateFormat("dd-MM-yyyy hh:mm a").format(date.value), style: context.textTheme.bodyMedium).tr(),
|
title: Text(DateFormat("dd-MM-yyyy hh:mm a").format(date.value), style: context.textTheme.bodyMedium),
|
||||||
onTap: pickDate,
|
onTap: pickDate,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
|||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||||
|
|
||||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||||
@@ -93,7 +95,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||||
context.pop();
|
context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()]));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -26,7 +25,7 @@ class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
|
|||||||
|
|
||||||
onDone(Set<BaseAsset> selected) {
|
onDone(Set<BaseAsset> selected) {
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
context.maybePop<Set<BaseAsset>>(selected);
|
context.pop<Set<BaseAsset>>(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline.provider.dart';
|
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||||
@@ -229,9 +230,7 @@ class _MapSheetDragRegion extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final assetsInBoundsText = assetsInBoundCount > 0
|
final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount});
|
||||||
? "map_assets_in_bounds".tr(namedArgs: {'count': assetsInBoundCount.toString()})
|
|
||||||
: "map_no_assets_in_bounds".tr();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user