mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 17:23:10 +03:00
Compare commits
48 Commits
tmp/demo-s
...
update-exi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edac67acd2 | ||
|
|
f410b58035 | ||
|
|
8943ec23ba | ||
|
|
04b03f2924 | ||
|
|
cf2c0260a6 | ||
|
|
ae8af84101 | ||
|
|
4794eeca88 | ||
|
|
ac65d46ec6 | ||
|
|
e5ca79dd44 | ||
|
|
49be6d7fd8 | ||
|
|
15c6506aee | ||
|
|
2c31a11e41 | ||
|
|
b6c5a03533 | ||
|
|
75bc32b47b | ||
|
|
fdbe6d649f | ||
|
|
2b131fe935 | ||
|
|
6ae24fbbd4 | ||
|
|
7f116d8e98 | ||
|
|
bd0840c411 | ||
|
|
a5123dec1a | ||
|
|
ffd18c5459 | ||
|
|
8242ff9bab | ||
|
|
8203b6c450 | ||
|
|
b352cf3336 | ||
|
|
96ed9a8c4a | ||
|
|
e7a5b96ed0 | ||
|
|
51c2c60231 | ||
|
|
43d585ce55 | ||
|
|
042da669d1 | ||
|
|
a724f147fe | ||
|
|
1e4b9ae5b7 | ||
|
|
99cddf1fd6 | ||
|
|
30d33f968f | ||
|
|
31ee19181a | ||
|
|
b58a450152 | ||
|
|
b87ba6865b | ||
|
|
565cceb323 | ||
|
|
f096dd0cc0 | ||
|
|
a3c3f9cfcb | ||
|
|
7b6a4be30c | ||
|
|
720189e2c2 | ||
|
|
dfab32c8f2 | ||
|
|
60174d662d | ||
|
|
8b6a765e12 | ||
|
|
2248a38567 | ||
|
|
97e52c5156 | ||
|
|
e8b4ac0522 | ||
|
|
548298b0c7 |
@@ -1,4 +1,4 @@
|
||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:2ef23730ec68d8511ec8e6e0b82550ca728b256805d81f60ed890f3bfb21cfb9
|
||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:b0b88ef6a5abf21194343d2c5b2829dddd9be1142f65f6a5e4390a51d5a70dd8
|
||||
FROM ${BASEIMAGE}
|
||||
|
||||
# Flutter SDK
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
|
||||
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
|
||||
uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3
|
||||
|
||||
# ℹ️ 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
|
||||
@@ -72,6 +72,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
|
||||
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -518,7 +518,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run existing migrations
|
||||
run: npm run typeorm:migrations:run
|
||||
run: npm run migrations:run
|
||||
|
||||
- name: Test npm run schema:reset command works
|
||||
run: npm run typeorm:schema:reset
|
||||
@@ -532,7 +532,7 @@ jobs:
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
server/src/migrations/
|
||||
server/src
|
||||
- name: Verify migration files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
|
||||
@@ -61,9 +61,7 @@
|
||||
|
||||
## Demo
|
||||
|
||||
Access the demo [here](https://demo.immich.app). The demo is running on a Free-tier Oracle VM in Amsterdam with a 2.4Ghz quad-core ARM64 CPU and 24GB RAM.
|
||||
|
||||
For the mobile app, you can use `https://demo.immich.app` for the `Server Endpoint URL`
|
||||
Access the demo [here](https://demo.immich.app). For the mobile app, you can use `https://demo.immich.app` for the `Server Endpoint URL`.
|
||||
|
||||
### Login credentials
|
||||
|
||||
|
||||
24
cli/package-lock.json
generated
24
cli/package-lock.json
generated
@@ -27,7 +27,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1362,13 +1362,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz",
|
||||
"integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==",
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@@ -4073,9 +4073,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4144,9 +4144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
|
||||
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
||||
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -116,7 +116,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
command: [ './run.sh', '-disable-reporting' ]
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.5.2-ubuntu@sha256:8b5858c447e06fd7a89006b562ba7bba7c4d5813600c7982374c41852adefaeb
|
||||
image: grafana/grafana:11.6.0-ubuntu@sha256:fd8fa48213c624e1a95122f1d93abbf1cf1cbe85fc73212c1e599dbd76c63ff8
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
# The location where your uploaded files are stored
|
||||
UPLOAD_LOCATION=./library
|
||||
# The location where your database files are stored
|
||||
|
||||
# The location where your database files are stored. Network shares are not supported for the database
|
||||
DB_DATA_LOCATION=./postgres
|
||||
|
||||
# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
@@ -31,7 +31,7 @@ Admin can send a welcome email if the Email option is set, you can learn here ho
|
||||
|
||||
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
|
||||
|
||||
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota using the value 0 (default).
|
||||
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota by leaving it empty (default).
|
||||
|
||||
:::tip
|
||||
The system administrator can see the usage quota percentage of all users in Server Stats page.
|
||||
|
||||
@@ -112,7 +112,7 @@ You begin by authenticating to your Immich server. For instance:
|
||||
immich login http://192.168.1.216:2283/api HFEJ38DNSDUEG
|
||||
```
|
||||
|
||||
This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
|
||||
This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/immich/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
|
||||
|
||||
Once you are authenticated, you can upload assets to your Immich server.
|
||||
|
||||
|
||||
@@ -70,3 +70,6 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc
|
||||
## Next Steps
|
||||
|
||||
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
|
||||
@@ -24,9 +24,6 @@ To clean up disk space, the old version's obsolete container images can be delet
|
||||
docker image prune
|
||||
```
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
|
||||
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
|
||||
[releases]: https://github.com/immich-app/immich/releases
|
||||
|
||||
61
e2e/package-lock.json
generated
61
e2e/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -25,7 +25,7 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^57.0.0",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
"exiftool-vendored": "^29.3.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
@@ -66,7 +66,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -100,7 +100,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1071,9 +1071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@photostructure/tz-lookup": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
|
||||
"integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==",
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.2.0.tgz",
|
||||
"integrity": "sha512-DwrvodcXHNSdGdeSF7SBL5o8aBlsaeuCuG7633F04nYsL3hn5Hxe3z/5kCqxv61J1q7ggKZ27GPylR3x0cPNXQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
@@ -1585,13 +1585,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz",
|
||||
"integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==",
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@@ -3312,27 +3312,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored": {
|
||||
"version": "28.8.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz",
|
||||
"integrity": "sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==",
|
||||
"version": "29.3.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-29.3.0.tgz",
|
||||
"integrity": "sha512-2N+QvQH3mH0yb89vpxJXaD+SXa8GXvDigytS6cro6FOrTx9Opav4H0QPP0V4r9dBhXy5poON7qo+p1KZv5wZqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@photostructure/tz-lookup": "^11.0.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@photostructure/tz-lookup": "^11.2.0",
|
||||
"@types/luxon": "^3.6.0",
|
||||
"batch-cluster": "^13.0.0",
|
||||
"he": "^1.2.0",
|
||||
"luxon": "^3.5.0"
|
||||
"luxon": "^3.6.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"exiftool-vendored.exe": "13.0.0",
|
||||
"exiftool-vendored.pl": "13.0.1"
|
||||
"exiftool-vendored.exe": "13.26.0",
|
||||
"exiftool-vendored.pl": "13.26.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored.exe": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.0.0.tgz",
|
||||
"integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==",
|
||||
"version": "13.26.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.26.0.tgz",
|
||||
"integrity": "sha512-y5mLmNAeABbXhb1a77EeR+CYDELI64DawnNywhMiivIYIdBPXf/81UcBd3SQlcTAHJjJjKt20qdmdL4loMkRDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -3341,15 +3341,18 @@
|
||||
]
|
||||
},
|
||||
"node_modules/exiftool-vendored.pl": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.0.1.tgz",
|
||||
"integrity": "sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==",
|
||||
"version": "13.26.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.26.0.tgz",
|
||||
"integrity": "sha512-4L8b6TrZcrd/dOnoeyCgsIa4WgFygucd0KQzB7xgWpgeMDQ2xYeqAYoHTeKmLAv4DogvaVkZQgDNogscuKuM+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"!win32"
|
||||
]
|
||||
],
|
||||
"bin": {
|
||||
"exiftool": "bin/exiftool"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.1",
|
||||
@@ -6316,9 +6319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -35,7 +35,7 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^57.0.0",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
"exiftool-vendored": "^29.3.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
|
||||
@@ -246,15 +246,7 @@ describe('/shared-links', () => {
|
||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets).toHaveLength(1);
|
||||
expect(body.assets[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
originalFileName: 'example.png',
|
||||
localDateTime: expect.any(String),
|
||||
fileCreatedAt: expect.any(String),
|
||||
exifInfo: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(body.assets).toHaveLength(0);
|
||||
expect(body.album).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ function imageLocator(page: Page) {
|
||||
test.describe('Photo Viewer', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset: AssetMediaResponseDto;
|
||||
let rawAsset: AssetMediaResponseDto;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
utils.initSdk();
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
asset = await utils.createAsset(admin.accessToken);
|
||||
rawAsset = await utils.createAsset(admin.accessToken, { assetData: { filename: 'test.arw' } });
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
@@ -36,7 +38,7 @@ test.describe('Photo Viewer', () => {
|
||||
await expect(page.getByTestId('loading-spinner')).toBeVisible();
|
||||
});
|
||||
|
||||
test('loads high resolution photo when zoomed', async ({ page }) => {
|
||||
test('loads original photo when zoomed', async ({ page }) => {
|
||||
await page.goto(`/photos/${asset.id}`);
|
||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
||||
const box = await imageLocator(page).boundingBox();
|
||||
@@ -44,6 +46,17 @@ test.describe('Photo Viewer', () => {
|
||||
const { x, y, width, height } = box!;
|
||||
await page.mouse.move(x + width / 2, y + height / 2);
|
||||
await page.mouse.wheel(0, -1);
|
||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('original');
|
||||
});
|
||||
|
||||
test('loads fullsize image when zoomed and original is web-incompatible', async ({ page }) => {
|
||||
await page.goto(`/photos/${rawAsset.id}`);
|
||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
||||
const box = await imageLocator(page).boundingBox();
|
||||
expect(box).toBeTruthy();
|
||||
const { x, y, width, height } = box!;
|
||||
await page.mouse.move(x + width / 2, y + height / 2);
|
||||
await page.mouse.wheel(0, -1);
|
||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('fullsize');
|
||||
});
|
||||
|
||||
|
||||
@@ -1374,4 +1374,4 @@
|
||||
"yes": "Да",
|
||||
"you_dont_have_any_shared_links": "Нямате споделени връзки",
|
||||
"zoom_image": "Увеличаване на изображението"
|
||||
}
|
||||
}
|
||||
|
||||
30
i18n/bi.json
30
i18n/bi.json
@@ -1,20 +1,22 @@
|
||||
{
|
||||
"account": "",
|
||||
"account_settings": "",
|
||||
"acknowledge": "",
|
||||
"about": "abaot",
|
||||
"account": "Akaont",
|
||||
"account_settings": "Seting blo Akaont",
|
||||
"acknowledge": "Akcept",
|
||||
"action": "",
|
||||
"actions": "",
|
||||
"active": "",
|
||||
"activity": "",
|
||||
"add": "",
|
||||
"add_a_description": "",
|
||||
"add_a_location": "",
|
||||
"add_a_name": "",
|
||||
"add_a_title": "",
|
||||
"add_exclusion_pattern": "",
|
||||
"add_import_path": "",
|
||||
"add_location": "",
|
||||
"add_more_users": "",
|
||||
"active": "Stap Mekem",
|
||||
"activity": "Wanem hemi Mekem",
|
||||
"activity_changed": "WAnem hemi Mekem hemi",
|
||||
"add": "Ad",
|
||||
"add_a_description": "Putem Description blo hem",
|
||||
"add_a_location": "Putem place blo hem",
|
||||
"add_a_name": "Putem nam blo hem",
|
||||
"add_a_title": "Putem wan name blo hem",
|
||||
"add_exclusion_pattern": "Putem wan paten wae hemi karem aot",
|
||||
"add_import_path": "Putem wan pat blo import",
|
||||
"add_location": "Putem wan place blo hem",
|
||||
"add_more_users": "Putem mor man",
|
||||
"add_partner": "",
|
||||
"add_path": "",
|
||||
"add_photos": "",
|
||||
|
||||
@@ -1374,4 +1374,4 @@
|
||||
"yes": "Sí",
|
||||
"you_dont_have_any_shared_links": "No tens cap enllaç compartit",
|
||||
"zoom_image": "Ampliar Imatge"
|
||||
}
|
||||
}
|
||||
|
||||
10
i18n/cs.json
10
i18n/cs.json
@@ -66,8 +66,13 @@
|
||||
"forcing_refresh_library_files": "Vynucení obnovy všech souborů knihovny",
|
||||
"image_format": "Formát",
|
||||
"image_format_description": "WebP vytváří menší soubory než JPEG, ale je pomalejší při kódování.",
|
||||
"image_fullsize_description": "Obrázek v plné velikosti s odstraněnými metadaty, použito při přiblížení",
|
||||
"image_fullsize_enabled": "Povolit generování obrázků v plné velikosti",
|
||||
"image_fullsize_enabled_description": "Generovat obrázky v plné velikosti pro formáty, které nejsou vhodné pro web. Pokud je povolena možnost „Preferovat vložený náhled“, budou přímo použity vložené náhledy bez převodu. Neovlivňuje formáty vhodné pro web, jako je JPEG.",
|
||||
"image_fullsize_quality_description": "Kvalita obrázku v plné velikosti od 1 do 100. Vyšší je lepší, ale vytváří větší soubory.",
|
||||
"image_fullsize_title": "Nastavení obrázků v plné velikosti",
|
||||
"image_prefer_embedded_preview": "Preferovat vložený náhled",
|
||||
"image_prefer_embedded_preview_setting_description": "Použít vložené náhledy z RAW fotografií jako vstup pro zpracování snímků, pokud jsou k dispozici. U některých snímků tak lze dosáhnout přesnějších barev, ale kvalita náhledu závisí na fotoaparátu a snímek může obsahovat více kompresních artefaktů.",
|
||||
"image_prefer_embedded_preview_setting_description": "Použít vložené náhledy z RAW fotografií jako vstup pro zpracování snímků a pokud jsou k dispozici. U některých snímků tak lze dosáhnout přesnějších barev, ale kvalita náhledu závisí na fotoaparátu a snímek může obsahovat více kompresních artefaktů.",
|
||||
"image_prefer_wide_gamut": "Preferovat široký gamut",
|
||||
"image_prefer_wide_gamut_setting_description": "Použít Display P3 pro miniatury. To lépe zachovává živost obrázků s širokým barevným prostorem, ale obrázky se mohou na starých zařízeních se starou verzí prohlížeče zobrazovat jinak. sRGB obrázky jsou ponechány jako sRGB, aby se zabránilo posunům barev.",
|
||||
"image_preview_description": "Středně velký obrázek se zbavenými metadaty, který se používá při prohlížení jedné položky a pro strojové učení",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Videa ve smyčce",
|
||||
"loop_videos_description": "Povolit automatickou smyčku videa v prohlížeči.",
|
||||
"main_branch_warning": "Používáte vývojovou verzi; důrazně doporučujeme používat verzi z vydání!",
|
||||
"main_menu": "Hlavní nabídka",
|
||||
"make": "Výrobce",
|
||||
"manage_shared_links": "Spravovat sdílené odkazy",
|
||||
"manage_sharing_with_partners": "Správa sdílení s partnery",
|
||||
@@ -1234,7 +1240,7 @@
|
||||
"sort_oldest": "Nejstarší fotka",
|
||||
"sort_people_by_similarity": "Seřadit lidi podle podobnosti",
|
||||
"sort_recent": "Nejnovější fotka",
|
||||
"sort_title": "Název",
|
||||
"sort_title": "Název alba",
|
||||
"source": "Zdroj",
|
||||
"stack": "Seskupit",
|
||||
"stack_duplicates": "Seskupit duplicity",
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Erneutes Laden aller Bibliotheksdateien erzwingen",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.",
|
||||
"image_fullsize_description": "Hochauflösendes Bild mit entfernten Metadaten, das beim Zoomen verwendet wird",
|
||||
"image_fullsize_enabled": "Hochauflösende Vorschaubilder aktivieren",
|
||||
"image_fullsize_enabled_description": "Generiere Hochauflösende Vorschaubilder in Originalauflösung für nicht web-kompatibel Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.",
|
||||
"image_fullsize_quality_description": "Qualität der Hochauflösenden Vorschaubilder von 1-100. Höher ist besser, erzeugt aber größere Dateien.",
|
||||
"image_fullsize_title": "Hochauflösende Vorschaueinstellungen",
|
||||
"image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
|
||||
"image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.",
|
||||
"image_prefer_wide_gamut": "Breites Spektrum bevorzugen",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Loop-Videos",
|
||||
"loop_videos_description": "Aktiviere diese Option, um eine automatische Videoschleife in der Detailansicht zu erstellen.",
|
||||
"main_branch_warning": "Du benutzt eine Entwicklungsversion. Wir empfehlen dringend, eine Release-Version zu verwenden!",
|
||||
"main_menu": "Hauptmenü",
|
||||
"make": "Marke",
|
||||
"manage_shared_links": "Freigegebene Links verwalten",
|
||||
"manage_sharing_with_partners": "Gemeinsame Nutzung mit Partnern verwalten",
|
||||
|
||||
@@ -388,7 +388,7 @@
|
||||
"albums_count": "{count, plural, one {{count, number} Άλμπουμ} other {{count, number} Άλμπουμ}}",
|
||||
"all": "Όλα",
|
||||
"all_albums": "Όλα τα άλμπουμ",
|
||||
"all_people": "Όλοι οι άνθρωποι",
|
||||
"all_people": "Όλα τα άτομα",
|
||||
"all_videos": "Όλα τα βίντεο",
|
||||
"allow_dark_mode": "Επιτρέψτε τη σκοτεινή λειτουργία",
|
||||
"allow_edits": "Επιτρέψτε τις τροποποιήσεις",
|
||||
@@ -452,7 +452,7 @@
|
||||
"camera_model": "Μοντέλο κάμερας",
|
||||
"cancel": "Ακύρωση",
|
||||
"cancel_search": "Ακύρωση αναζήτησης",
|
||||
"cannot_merge_people": "Αδύνατη η συγχώνευση προσώπων",
|
||||
"cannot_merge_people": "Αδύνατη η συγχώνευση ατόμων",
|
||||
"cannot_undo_this_action": "Δεν μπορείτε να αναιρέσετε αυτήν την ενέργεια!",
|
||||
"cannot_update_the_description": "Αδύνατη η ενημέρωση της περιγραφής",
|
||||
"change_date": "Αλλαγή ημερομηνίας",
|
||||
@@ -618,7 +618,7 @@
|
||||
"cant_change_metadata_assets_count": "Δεν μπορείτε να αλλάξετε τα μεταδεδομένα του {count, plural, one {# αρχείου} other {# αρχείων}}",
|
||||
"cant_get_faces": "Δεν είναι δυνατή η ανάκτηση προσώπων",
|
||||
"cant_get_number_of_comments": "Δεν είναι δυνατή η ανάκτηση του αριθμού των σχολίων",
|
||||
"cant_search_people": "Δεν μπορείτε να αναζητήσετε άτομα",
|
||||
"cant_search_people": "Αδύνατη η αναζήτηση ατόμων",
|
||||
"cant_search_places": "Δεν μπορείτε να αναζητήσετε τοποθεσίες",
|
||||
"cleared_jobs": "Εκκαθαρισμένες εργασίες για: {job}",
|
||||
"error_adding_assets_to_album": "Σφάλμα κατά την προσθήκη στοιχείων στο άλμπουμ",
|
||||
|
||||
@@ -164,7 +164,6 @@
|
||||
"no_pattern_added": "No pattern added",
|
||||
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||
"note_cannot_be_changed_later": "NOTE: This cannot be changed later!",
|
||||
"note_unlimited_quota": "Note: Enter 0 for unlimited quota",
|
||||
"notification_email_from_address": "From address",
|
||||
"notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server <noreply@example.com>\"",
|
||||
"notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)",
|
||||
@@ -929,7 +928,6 @@
|
||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||
"not_in_any_album": "Not in any album",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||
"note_unlimited_quota": "Note: Enter 0 for unlimited quota",
|
||||
"notes": "Notes",
|
||||
"notification_toggle_setting_description": "Enable email notifications",
|
||||
"notifications": "Notifications",
|
||||
@@ -1384,4 +1382,4 @@
|
||||
"yes": "Yes",
|
||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||
"zoom_image": "Zoom Image"
|
||||
}
|
||||
}
|
||||
|
||||
16
i18n/es.json
16
i18n/es.json
@@ -66,8 +66,13 @@
|
||||
"forcing_refresh_library_files": "Forzando la recarga de todos los elementos en la biblioteca",
|
||||
"image_format": "Formato",
|
||||
"image_format_description": "WebP genera archivos más pequeños que JPEG, pero es más lento al codificarlos.",
|
||||
"image_fullsize_description": "Imagen de tamaño completo con metadatos removidos, usado cuando se hace zoom",
|
||||
"image_fullsize_enabled": "Activar generación de imágenes a tamaño completo",
|
||||
"image_fullsize_enabled_description": "Generar imágenes a tamaño completo para formatos no aptos para web. Cuando \"Preferir vista previa incrustada\" está activada, las vistas previas incrustadas se utilizan directamente sin conversión. No afecta a los formatos aptos para la web, como JPEG.",
|
||||
"image_fullsize_quality_description": "De 1 a 100, calidad de imágenes de tamaño completo. Mientras más alto es mejor, pero genera archivos de mayor tamaño.",
|
||||
"image_fullsize_title": "Configuraciones de imágenes de tamaño completo",
|
||||
"image_prefer_embedded_preview": "Preferir vista previa embebida",
|
||||
"image_prefer_embedded_preview_setting_description": "Usar vistas previas embebidas en fotos RAW como entrada para el procesamiento de imágenes cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.",
|
||||
"image_prefer_embedded_preview_setting_description": "Usar vistas previas embebidas en fotos RAW como entrada para el procesamiento de imágenes y cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.",
|
||||
"image_prefer_wide_gamut": "Preferir 'gamut' amplio",
|
||||
"image_prefer_wide_gamut_setting_description": "Usar \"Display P3\" para las miniaturas. Preserva mejor la vivacidad de las imágenes con espacios de color amplios pero las imágenes pueden aparecer de manera diferente en dispositivos antiguos con una versión antigua del navegador. Las imágenes sRGB se mantienen como sRGB para evitar cambios de color.",
|
||||
"image_preview_description": "Imagen de tamaño mediano con metadatos eliminados. Es utilizado al visualizar un solo activo y para el aprendizaje automático",
|
||||
@@ -414,7 +419,7 @@
|
||||
"asset_description_updated": "La descripción del elemento ha sido actualizada",
|
||||
"asset_filename_is_offline": "El archivo {filename} está offline",
|
||||
"asset_has_unassigned_faces": "El archivo no tiene rostros asignados",
|
||||
"asset_hashing": "Hashing…",
|
||||
"asset_hashing": "Calculando hash…",
|
||||
"asset_offline": "Archivos sin conexión",
|
||||
"asset_offline_description": "Este activo externo ya no se encuentra en el disco. Por favor, póngase en contacto con su administrador de Immich para obtener ayuda.",
|
||||
"asset_skipped": "Omitido",
|
||||
@@ -442,7 +447,7 @@
|
||||
"blurred_background": "Fondo borroso",
|
||||
"bugs_and_feature_requests": "Errores y solicitudes de funciones",
|
||||
"build": "Compilación",
|
||||
"build_image": "Crear imagen",
|
||||
"build_image": "Imagen de compilación",
|
||||
"bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!",
|
||||
"bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.",
|
||||
"bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Vídeos en bucle",
|
||||
"loop_videos_description": "Habilite la reproducción automática de un video en el visor de detalles.",
|
||||
"main_branch_warning": "Estás ejecutando una compilación desde la rama principal. ¡Recomendamos encarecidamente usar una versión de lanzamiento!",
|
||||
"main_menu": "Menú principal",
|
||||
"make": "Marca",
|
||||
"manage_shared_links": "Administrar enlaces compartidos",
|
||||
"manage_sharing_with_partners": "Administrar el uso compartido con invitados",
|
||||
@@ -1241,7 +1247,7 @@
|
||||
"stack_select_one_photo": "Selecciona una imagen principal para la pila",
|
||||
"stack_selected_photos": "Apilar fotos seleccionadas",
|
||||
"stacked_assets_count": "Apilado(s) {count, plural, one {# activo} other {# activos}}",
|
||||
"stacktrace": "Stacktrace",
|
||||
"stacktrace": "Seguimiento de pila",
|
||||
"start": "Inicio",
|
||||
"start_date": "Fecha de inicio",
|
||||
"state": "Estado",
|
||||
@@ -1350,7 +1356,7 @@
|
||||
"version_announcement_closing": "Tu amigo, Alex",
|
||||
"version_announcement_message": "¡Hola! Hay una nueva versión de Immich disponible. Tómese un tiempo para leer las <link> notas de la versión </link> para asegurarse de que su configuración esté actualizada y evitar errores de configuración, especialmente si utiliza WatchTower o cualquier mecanismo que se encargue de actualizar su instancia de Immich automáticamente.",
|
||||
"version_history": "Historial de versiones",
|
||||
"version_history_item": "Instalada la {version} el {date}",
|
||||
"version_history_item": "Instalada {version} el {date}",
|
||||
"video": "Vídeo",
|
||||
"video_hover_setting": "Iniciar vídeo al pasar por encima",
|
||||
"video_hover_setting_description": "Reproducir el vídeo cuando el ratón está encima de un vídeo. Aunque esté desactivado, se iniciará cuando el cursor del ratón esté sobre el icono de \"reproducir\".",
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Kogu kõigi failide sundvärskendamine",
|
||||
"image_format": "Formaat",
|
||||
"image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.",
|
||||
"image_fullsize_description": "Täismõõdus pilt ilma metaandmeteta, kasutatakse sisse suumimisel",
|
||||
"image_fullsize_enabled": "Luba täismõõdus piltide genereerimine",
|
||||
"image_fullsize_enabled_description": "Genereeri mitte-veebisõbralike formaatide jaoks täismõõdus pilt. Kui \"Eelista manustatud eelvaadet\" on lubatud, kasutatakse manustatud eelvaateid otse ilma teisendamiseta. Ei mõjuta veebisõbralikke formaate nagu JPEG.",
|
||||
"image_fullsize_quality_description": "Täismõõdus pildi kvaliteet vahemikus 1-100. Kõrgem väärtus on parem, aga tulemuseks on suuremad failid.",
|
||||
"image_fullsize_title": "Täismõõdus pildi seaded",
|
||||
"image_prefer_embedded_preview": "Eelista manustatud eelvaadet",
|
||||
"image_prefer_embedded_preview_setting_description": "Kasuta pilditöötluse sisendina võimalusel RAW fotodesse manustatud eelvaateid. See võib mõnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sõltub konkreetsest kaamerast ning pildis võib olla rohkem tihendusmüra.",
|
||||
"image_prefer_wide_gamut": "Eelista laia värvigammat",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Taasesita videod",
|
||||
"loop_videos_description": "Lülita sisse, et detailvaates videot automaatselt taasesitada.",
|
||||
"main_branch_warning": "Sa kasutad arendusversiooni; soovitame tungivalt kasutada väljalaskeversiooni!",
|
||||
"main_menu": "Peamenüü",
|
||||
"make": "Mark",
|
||||
"manage_shared_links": "Halda jagatud linke",
|
||||
"manage_sharing_with_partners": "Halda partneritega jagamist",
|
||||
|
||||
@@ -926,4 +926,4 @@
|
||||
"yes": "بله",
|
||||
"you_dont_have_any_shared_links": "",
|
||||
"zoom_image": "بزرگنمایی تصویر"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
"reset_settings_to_default": "Nollaa asetukset oletuksille",
|
||||
"reset_settings_to_recent_saved": "Palauta aiemmin tallennetut asetukset",
|
||||
"scanning_library": "Kirjastoa skannataan",
|
||||
"search_jobs": "Etsi tehtäviä...",
|
||||
"search_jobs": "Etsi tehtäviä…",
|
||||
"send_welcome_email": "Lähetä tervetuloviesti",
|
||||
"server_external_domain_settings": "Ulkoinen osoite",
|
||||
"server_external_domain_settings_description": "Osoite julkisille linkeille, http(s):// mukaan lukien",
|
||||
@@ -406,11 +406,11 @@
|
||||
"are_these_the_same_person": "Ovatko he sama henkilö?",
|
||||
"are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?",
|
||||
"asset_added_to_album": "Lisätty albumiin",
|
||||
"asset_adding_to_album": "Lisätään albumiin...",
|
||||
"asset_adding_to_album": "Lisätään albumiin…",
|
||||
"asset_description_updated": "Kohteen kuvaus on päivitetty",
|
||||
"asset_filename_is_offline": "Kohde {filename} on offline-tilassa",
|
||||
"asset_has_unassigned_faces": "Kohteella on määrittämättömiä kasvoja",
|
||||
"asset_hashing": "Hajautetaan...",
|
||||
"asset_hashing": "Hajautetaan…",
|
||||
"asset_offline": "Aineisto offline-tilassa",
|
||||
"asset_offline_description": "Tätä ulkoista resurssia ei enää löydy levyltä. Ole hyvä ja ota yhteyttä Immich-järjestelmänvalvojaan saadaksesi apua.",
|
||||
"asset_skipped": "Ohitettu",
|
||||
@@ -1352,4 +1352,4 @@
|
||||
"yes": "Kyllä",
|
||||
"you_dont_have_any_shared_links": "Sinulla ei ole jaettuja linkkejä",
|
||||
"zoom_image": "Zoomaa kuvaa"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Forcer le rafraîchissement de tous les fichiers de la bibliothèque",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP produit des fichiers plus petits que JPEG, mais son encodage est plus lent.",
|
||||
"image_fullsize_description": "Image en taille réelle, sans métadonnées, utilisée lors d'un zoom",
|
||||
"image_fullsize_enabled": "Activer la génération d'image en taille d'origine",
|
||||
"image_fullsize_enabled_description": "Générer une image en taille réelle pour les formats non compatibles avec le web. Lorsque l'option « Préférer l'aperçu intégré » est activée, les aperçus intégrés sont utilisés directement sans conversion. Cette option n'affecte pas les formats compatibles avec le web tels que JPEG.",
|
||||
"image_fullsize_quality_description": "Qualité de l'image en taille réelle de 1 à 100. Une valeur plus élevée est meilleure, mais produit des fichiers plus volumineux.",
|
||||
"image_fullsize_title": "Paramètres des images en taille réelle",
|
||||
"image_prefer_embedded_preview": "Préférer l'aperçu intégré",
|
||||
"image_prefer_embedded_preview_setting_description": "Utiliser les miniatures intégrées dans les photos au format RAW comme entrées pour le traitement d'image quand elles sont disponibles. Cela peut donner des couleurs plus justes pour certaines images, mais la qualité des miniatures est dépendant de l'appareil photo et l'image peut avoir des artéfacts de compression.",
|
||||
"image_prefer_wide_gamut": "Préférer une gamme de couleurs étendue",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Vidéos en boucle",
|
||||
"loop_videos_description": "Activer pour voir la vidéo en boucle dans le lecteur détaillé.",
|
||||
"main_branch_warning": "Vous utilisez une version de développement. Nous vous recommandons fortement d'utiliser une version stable !",
|
||||
"main_menu": "Menu principal",
|
||||
"make": "Marque",
|
||||
"manage_shared_links": "Gérer les liens partagés",
|
||||
"manage_sharing_with_partners": "Gérer le partage avec les partenaires",
|
||||
|
||||
58
i18n/gl.json
58
i18n/gl.json
@@ -1 +1,57 @@
|
||||
{}
|
||||
{
|
||||
"about": "Acerca de",
|
||||
"account": "Conta",
|
||||
"account_settings": "Configuración da conta",
|
||||
"acknowledge": "De acordo",
|
||||
"action": "Acción",
|
||||
"actions": "Accións",
|
||||
"active": "Activo",
|
||||
"activity": "Actividade",
|
||||
"activity_changed": "A actividade está {enabled, select, true {habilitada} other {deshabilitada}}",
|
||||
"add": "Engadir",
|
||||
"add_a_description": "Engadir unha descrición",
|
||||
"add_a_location": "Engadir unha localización",
|
||||
"add_a_name": "Engadir un nome",
|
||||
"add_a_title": "Engadir un título",
|
||||
"add_exclusion_pattern": "Engadir patrón de exclusión",
|
||||
"add_import_path": "Engadir ruta de importación",
|
||||
"add_location": "Engadir localización",
|
||||
"add_more_users": "Engadir máis usuarios",
|
||||
"add_partner": "Engadir compañeiro",
|
||||
"add_path": "Engadir ruta",
|
||||
"add_photos": "Engadir fotos",
|
||||
"add_to": "Engadir a…",
|
||||
"add_to_album": "Engadir ao álbum",
|
||||
"add_to_shared_album": "Engadir ao álbum compartido",
|
||||
"add_url": "Engadir URL",
|
||||
"added_to_archive": "Engadido ao arquivo",
|
||||
"added_to_favorites": "Engadido a favoritos",
|
||||
"added_to_favorites_count": "Engadidos {count, number} a favoritos",
|
||||
"admin": {
|
||||
"authentication_settings": "Configuración de autenticación",
|
||||
"authentication_settings_description": "Xestionar contrasinal, OAuth e outros parámetros de autenticación",
|
||||
"authentication_settings_disable_all": "Estás seguro de deshabilitar todos os métodos de inicio de sesión? Iniciar a sesión quedará completamente deshabilitado.",
|
||||
"authentication_settings_reenable": "Para rehabilitala, usa un <link>Comando do servidor</link>.",
|
||||
"background_task_job": "Tarefas en segundo plano",
|
||||
"backup_database": "Respaldo da base de datos",
|
||||
"backup_database_enable_description": "Habilitar as copias de seguridade da base de datos",
|
||||
"backup_keep_last_amount": "Cantidade de copias de seguridade previas a manter",
|
||||
"backup_settings": "Configuración de copias de seguridade",
|
||||
"backup_settings_description": "Xestionar a configuración das copias de seguridade da base de datos",
|
||||
"check_all": "Comprobar todo",
|
||||
"cleared_jobs": "Traballos borrados para: {job}",
|
||||
"config_set_by_file": "As configuracións están actualmente seleccionadas por un ficheiro de configuracións",
|
||||
"confirm_delete_library": "Estás seguro de que queres eliminar a biblioteca {library}?",
|
||||
"exclusion_pattern_description": "Os patróns de exclusión permítenche ignorar ficheiros e cartafoles ao escanear a túa biblioteca. Isto é útil se tes cartafoles que conteñen ficheiros que non queres importar, coma ficheiros RAW.",
|
||||
"external_library_created_at": "Biblioteca externa (creada o {date})",
|
||||
"external_library_management": "Xestión de bibliotecas externas",
|
||||
"face_detection": "Detección de caras",
|
||||
"job_settings": "Configuración de tarefas",
|
||||
"job_settings_description": "Administrar tarefas simultáneas",
|
||||
"job_status": "Estado da tarefa",
|
||||
"jobs_failed": "{jobCount, one {# errado}, plural, other {# errados}}"
|
||||
},
|
||||
"year": "Ano",
|
||||
"yes": "Si",
|
||||
"zoom_image": "Acercar imaxe"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "כפיית רענון של כל קבצי הספרייה",
|
||||
"image_format": "פורמט",
|
||||
"image_format_description": "WebP מפיק קבצים קטנים יותר מ JPEG, אך הוא איטי יותר לקידוד.",
|
||||
"image_fullsize_description": "תמונה בגודל מלא עם מטא נתונים מוסרים, בעת שימוש בהגדלה",
|
||||
"image_fullsize_enabled": "אפשר יצירה של תמונות באיכות מלאה",
|
||||
"image_fullsize_enabled_description": "צור תמונה בגודל מלא עבור פורמטים שאינם ידידותיים לאינטרנט. כאשר \"העדף תצוגה מקדימה מוטמעת\" מופעלת, תצוגות מקדימות מוטמעות משמשות ישירות ללא המרה. זה לא משפיע על פורמטים ידידותיים לאינטרנט כמו JPEG.",
|
||||
"image_fullsize_quality_description": "תמונה בגודל מלא באיכות מ 1-100. גבוהה יותר טוב יותר, אך מייצר קובץ גדול יותר.",
|
||||
"image_fullsize_title": "הגדרות תמונה בגודל מלא",
|
||||
"image_prefer_embedded_preview": "העדף תצוגה מקדימה מוטמעת",
|
||||
"image_prefer_embedded_preview_setting_description": "השתמש בתצוגות מקדימות מוטמעות בתמונות RAW כקלט לעיבוד תמונה כאשר זמינות. זה יכול להפיק צבעים מדויקים יותר עבור תמונות מסוימות, אבל האיכות של התצוגה המקדימה היא תלוית מצלמה ולתמונה עשויים להיות יותר פגמי דחיסה.",
|
||||
"image_prefer_wide_gamut": "העדף סולם צבעים רחב",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "הפעלה חוזרת של סרטונים",
|
||||
"loop_videos_description": "אפשר הפעלה חוזרת אוטומטית של סרטון במציג הפרטים.",
|
||||
"main_branch_warning": "את/ה משתמש/ת בגרסת פיתוח; אנחנו ממליצים בחום להשתמש בגרסה יציבה!",
|
||||
"main_menu": "תפריט ראשי",
|
||||
"make": "תוצרת",
|
||||
"manage_shared_links": "ניהול קישורים משותפים",
|
||||
"manage_sharing_with_partners": "ניהול שיתוף עם שותפים",
|
||||
|
||||
@@ -1253,4 +1253,4 @@
|
||||
"yes": "",
|
||||
"you_dont_have_any_shared_links": "",
|
||||
"zoom_image": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "A képtár összes fájljának frissítése",
|
||||
"image_format": "Formátum",
|
||||
"image_format_description": "WebP a JPEG-nél kisebb fájlokat készít, de lassabban.",
|
||||
"image_fullsize_description": "Teljes méretű kép eltávolított metaadatokkal, nagyításkor használva",
|
||||
"image_fullsize_enabled": "Teljes méretű képgenerálás engedélyezése",
|
||||
"image_fullsize_enabled_description": "Teljes méretű kép generálása nem webbarát formátumokhoz. Ha a „Beágyazott előnézet preferálása” engedélyezve van, a beágyazott előnézetek közvetlenül, átalakítás nélkül kerülnek felhasználásra. Nem érinti a webbarát formátumokat, például a JPEG-et.",
|
||||
"image_fullsize_quality_description": "Teljes méretű képminőség 1-100 között. A magasabb érték jobb minőséget eredményez, de nagyobb fájlméretet is.",
|
||||
"image_fullsize_title": "Teljes méretű képbeállítások",
|
||||
"image_prefer_embedded_preview": "Beágyazott előnézeti kép előnyben részesítése",
|
||||
"image_prefer_embedded_preview_setting_description": "Nyers (RAW) fotók esetén használja a beépített előnézeti képet (ha van) a képek feldogozásához. Ez néhány kép esetében pontosabb színeket eredményezhet, de az előnézeti kép minősége erősen fényképezőgép függő, és a képen előfordulhatnak tömörítési hibák.",
|
||||
"image_prefer_wide_gamut": "Széles színtér preferálása",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Videók ismétlése",
|
||||
"loop_videos_description": "Engedélyezi a videók folyamatosan ismételt lejátszását.",
|
||||
"main_branch_warning": "Fejlesztői verziót használsz. Javasoljuk a stabil verzió használatát!",
|
||||
"main_menu": "Főmenü",
|
||||
"make": "Gyártó",
|
||||
"manage_shared_links": "Megosztási linkek kezelése",
|
||||
"manage_sharing_with_partners": "Partnerekkel való megosztás kezelése",
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Forzando l'aggiornamento completo della libreria",
|
||||
"image_format": "Formato",
|
||||
"image_format_description": "WebP produce file più piccoli rispetto a JPEG, ma l'encoding è più lento.",
|
||||
"image_fullsize_description": "Le immagini con dimensioni reali senza metadati sono utilizzate durante lo zoom",
|
||||
"image_fullsize_enabled": "Abilita la generazione delle immagini con dimensioni reali",
|
||||
"image_fullsize_enabled_description": "Genera immagini con dimensioni reali per i formati non web-friendly. Quando \"Preferisci l'anteprima integrata\" è abilitata, le anteprime integrate saranno usate senza conversione. Non riguarda le immagini web-friendly come il JPEG.",
|
||||
"image_fullsize_quality_description": "Qualità delle immagini con dimensioni reali da 1 a 100. Più è alto il valore più la qualità sarà alta come anche la grandezza dei file.",
|
||||
"image_fullsize_title": "Impostazioni Immagini con dimensioni reali",
|
||||
"image_prefer_embedded_preview": "Preferisci l'anteprima integrata",
|
||||
"image_prefer_embedded_preview_setting_description": "Usa l'anteprima integrata nelle foto RAW come input per l'elaborazione delle immagini, se disponibile. Questo permette un miglioramento dei colori per alcune immagini, ma la qualità delle anteprime dipende dalla macchina fotografica. Inoltre le immagini potrebbero presentare artefatti di compressione.",
|
||||
"image_prefer_wide_gamut": "Preferisci gamut più ampio",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Riproduci video in loop",
|
||||
"loop_videos_description": "Abilita per riprodurre automaticamente un video in loop nella vista dettagli.",
|
||||
"main_branch_warning": "Stai usando una versione di sviluppo. Consigliamo vivamente di utilizzare una versione di rilascio!",
|
||||
"main_menu": "Menu Principale",
|
||||
"make": "Produttore",
|
||||
"manage_shared_links": "Gestisci link condivisi",
|
||||
"manage_sharing_with_partners": "Gestisci la condivisione con i compagni",
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
"forcing_refresh_library_files": "すべてのライブラリファイルを強制更新",
|
||||
"image_format": "フォーマット",
|
||||
"image_format_description": "WebPはJPEGよりもファイルサイズが小さいですが、エンコードに時間がかかります。",
|
||||
"image_fullsize_enabled": "原寸大画像生成を有効にする",
|
||||
"image_fullsize_quality_description": "1から100まで原寸大画像の質です。高いほうがいいがファイルが大きくなります。",
|
||||
"image_fullsize_title": "原寸大画像設定",
|
||||
"image_prefer_embedded_preview": "埋め込みプレビューを優先",
|
||||
"image_prefer_embedded_preview_setting_description": "RAW写真の埋め込みプレビューが利用可能な場合に画像処理の入力として使用します。これにより、いくつかの画像でより正確な色を得ることができますが、プレビューの品質はカメラによって異なり、画像により多くの圧縮アーティファクトが含まれる場合があります。",
|
||||
"image_prefer_wide_gamut": "広色域に対応させる",
|
||||
@@ -859,6 +862,7 @@
|
||||
"loop_videos": "動画をループ",
|
||||
"loop_videos_description": "有効にすると詳細表示で自動的に動画がループします。",
|
||||
"main_branch_warning": "開発版を使っているようです。リリース版の使用を強く推奨します!",
|
||||
"main_menu": "メインメニュー",
|
||||
"make": "メーカー",
|
||||
"manage_shared_links": "共有済みのリンクを管理",
|
||||
"manage_sharing_with_partners": "パートナーとの共有を管理します",
|
||||
|
||||
166
i18n/ka.json
166
i18n/ka.json
@@ -1 +1,165 @@
|
||||
{}
|
||||
{
|
||||
"about": "შესახებ",
|
||||
"account": "ანგარიში",
|
||||
"account_settings": "ანგარიშის პარამეტრები",
|
||||
"acknowledge": "მიღება",
|
||||
"action": "ქმედება",
|
||||
"actions": "ქმედებები",
|
||||
"active": "აქტიური",
|
||||
"activity": "აქტივობა",
|
||||
"add": "დამატება",
|
||||
"add_a_description": "დაამატე აღწერა",
|
||||
"add_a_location": "დაამატე ადგილი",
|
||||
"add_a_name": "დაამატე სახელი",
|
||||
"add_a_title": "დაასათაურე",
|
||||
"add_import_path": "დაამატე საიმპორტო მისამართი",
|
||||
"add_location": "დაამატე ადგილი",
|
||||
"add_more_users": "დაამატე მომხმარებლები",
|
||||
"add_partner": "დაამატე პარტნიორი",
|
||||
"add_path": "დაამატე მისამართი",
|
||||
"add_photos": "დაამატე ფოტოები",
|
||||
"add_to_album": "დაამატე ალბომში",
|
||||
"add_to_shared_album": "დაამატე საზიარო ალბომში",
|
||||
"add_url": "დაამატე URL",
|
||||
"added_to_archive": "დაარქივდა",
|
||||
"added_to_favorites": "დაამატე რჩეულებში",
|
||||
"added_to_favorites_count": "{count, number} დაემატა რჩეულებში",
|
||||
"admin": {
|
||||
"authentication_settings": "ავთენტიკაციის პარამეტრები",
|
||||
"authentication_settings_description": "პაროლის, OAuth-ის და სხვა ავტენთიფიკაციის პარამეტრების მართვა",
|
||||
"authentication_settings_disable_all": "ნამდვილად გინდა ავტორიზაციის ყველა მეთოდის გამორთვა? ავტორიზაციას ვეღარანაირად შეძლებ.",
|
||||
"authentication_settings_reenable": "რეაქტივაციისთვის, გამოიყენე <link>სერვერის ბრძანება</link>.",
|
||||
"background_task_job": "ფონური დავალებები",
|
||||
"backup_database": "შექმენი სარეზერვო ასლი",
|
||||
"backup_database_enable_description": "ჩართე სარეზერვო ასლების ფუნქცია",
|
||||
"backup_keep_last_amount": "შესანახი სარეზერვო ასლების რაოდენობა",
|
||||
"backup_settings": "სარეზერვო ასლების პარამეტრები",
|
||||
"backup_settings_description": "მონაცემთა ბაზის სარეზერვო ასლების პარამეტრების მართვა",
|
||||
"check_all": "შეამოწმე ყველა",
|
||||
"cleanup": "გასუფთავება",
|
||||
"confirm_delete_library": "ნამდვილად გინდა {library} ბიბლიოთეკის წაშლა?",
|
||||
"confirm_email_below": "დასადასტურებლად, ქვემოთ აკრიფე \"{email}\"",
|
||||
"confirm_user_password_reset": "ნამდვილად გინდა {user}-(ი)ს პაროლის დარესეტება?",
|
||||
"disable_login": "გამორთე ავტორიზაცია",
|
||||
"external_library_management": "გარე ბიბლიოთეკების მართვა",
|
||||
"face_detection": "სახის ამოცნობა",
|
||||
"image_format": "ფორმატი",
|
||||
"image_fullsize_title": "სრული ზომის გამოსახულების პარამეტრები",
|
||||
"image_quality": "ხარისხი",
|
||||
"image_resolution": "გაფართოება",
|
||||
"image_settings": "გამოსახულების პარამეტრები",
|
||||
"image_settings_description": "გენერირებული ფოტოების ხარისხისა და რეზოლუციის მართვა",
|
||||
"image_thumbnail_description": "მინიატურა მეტაინფორმაციის გარეშე, რომელიც ფოტოები ჯგუფურად თვალიერებისას გამოიყენება(მაგ. მთავარ თაიმლაინზე)",
|
||||
"image_thumbnail_quality_description": "მინიატურის ხარისხი 1-დან 100-მდე. დიდი რიცხვი შეესაბამება უკეთეს ხარისხს, თუმცა, უფრო დიდ ფაილებს და აპლიკაციის შესაძლო შენელებას.",
|
||||
"image_thumbnail_title": "მინიატურის პარამეტრები",
|
||||
"library_created": "შეიქმნა ბიბლიოთეკა: {library}",
|
||||
"library_deleted": "ბიბლიოთეკა წაიშალა",
|
||||
"library_import_path_description": "აირჩიე დასაიმპორტებელი საქაღალდე. ფოტოები და ვიდეოები მოიძებნება ამ საქაღალდესა და მასში არსებულ საქაღალდეებში.",
|
||||
"library_settings_description": "გარე ბიბლიოთეკების პარამეტრების მართვა",
|
||||
"logging_settings": "ჟურნალი",
|
||||
"map_settings": "რუკა",
|
||||
"migration_job": "მიგრაცია",
|
||||
"oauth_scope": "დიაპაზონი",
|
||||
"oauth_settings": "OAuth",
|
||||
"template_email_preview": "მინიატურა",
|
||||
"transcoding_acceleration_vaapi": "VAAPI",
|
||||
"transcoding_threads": "ნაკადები",
|
||||
"transcoding_tone_mapping": "ტონების ასახვა"
|
||||
},
|
||||
"administration": "ადმინისტრაცია",
|
||||
"advanced": "დამატებით",
|
||||
"albums": "ალბომები",
|
||||
"all": "ყველა",
|
||||
"anti_clockwise": "საათის ისრის საწინააღმდეგო",
|
||||
"archive": "არქივი",
|
||||
"asset_hashing": "დაჰეშვა.…",
|
||||
"asset_skipped": "გამოტოვებულია",
|
||||
"asset_uploaded": "ატვირთულია",
|
||||
"asset_uploading": "მიმდინარეობს ატვირთვა…",
|
||||
"assets": "ობიექტები",
|
||||
"back": "უკან",
|
||||
"backward": "უკან გადასვლა",
|
||||
"build": "აგება",
|
||||
"camera": "კამერა",
|
||||
"cancel": "გაუქმება",
|
||||
"city": "ქალაქი",
|
||||
"clear": "გასუფთავება",
|
||||
"clockwise": "საათის ისრის მიმართულებით",
|
||||
"close": "დახურვა",
|
||||
"collapse": "აკეცვა",
|
||||
"color": "ფერი",
|
||||
"confirm": "დასტური",
|
||||
"contain": "შეიცავს",
|
||||
"context": "კონტექსტი",
|
||||
"continue": "გაგრძელება",
|
||||
"country": "ქვეყანა",
|
||||
"cover": "ყდა",
|
||||
"covers": "ყდები",
|
||||
"create": "შექმნა",
|
||||
"created": "შექმნილია",
|
||||
"dark": "მუქი",
|
||||
"day": "დღე",
|
||||
"delete": "წაშლა",
|
||||
"description": "აღწერა",
|
||||
"details": "დეტალები",
|
||||
"direction": "მიმართულება",
|
||||
"disabled": "გათიშულია",
|
||||
"discord": "Discord",
|
||||
"discover": "აღმოჩენა",
|
||||
"documentation": "დოკუმენტაცია",
|
||||
"done": "მზადაა",
|
||||
"download": "გადმოწერა",
|
||||
"download_settings": "გადმოწერა",
|
||||
"downloading": "მიმდინარეობს გადმოწერა",
|
||||
"duplicates": "დუბლიკატები",
|
||||
"duration": "ხანგრძლივობა",
|
||||
"edit": "ჩასწორება",
|
||||
"edited": "ჩასწორებულია",
|
||||
"editor": "რედაქტორი",
|
||||
"editor_crop_tool_h2_rotation": "ტრიალი",
|
||||
"email": "ელფოსტა",
|
||||
"enable": "ჩართვა",
|
||||
"enabled": "ჩართულია",
|
||||
"error": "შეცდომა",
|
||||
"exif": "Exif",
|
||||
"expired": "ვადაამოწურულია",
|
||||
"explore": "დათვალიერება",
|
||||
"explorer": "გამცილებელი",
|
||||
"export": "გატანა",
|
||||
"extension": "გაფართოება",
|
||||
"external": "გარე",
|
||||
"face_unassigned": "მიუნიჭებელი",
|
||||
"favorite": "რჩეული",
|
||||
"favorites": "რჩეულები",
|
||||
"features": "თვისებები",
|
||||
"filename": "ფაილის სახელი",
|
||||
"filetype": "ფაილის ტიპი",
|
||||
"folders": "საქაღალდეები",
|
||||
"forward": "წინ",
|
||||
"general": "ზოგადი",
|
||||
"host": "ჰოსტი",
|
||||
"hour": "საათი",
|
||||
"image": "გამოსახულება",
|
||||
"info": "ინფორმაცია",
|
||||
"jobs": "დავალებები",
|
||||
"keep": "შენარჩუნება",
|
||||
"language": "ენა",
|
||||
"latitude": "განედი",
|
||||
"leave": "გასვლა",
|
||||
"level": "დონე",
|
||||
"library": "ბიბლიოთეკა",
|
||||
"light": "ღია",
|
||||
"list": "სია",
|
||||
"loading": "ჩატვირთვა",
|
||||
"login": "შესვლა",
|
||||
"longitude": "გრძედი",
|
||||
"look": "შეხედვა",
|
||||
"make": "მწარმოებელი",
|
||||
"map": "რუკა",
|
||||
"matches": "დამთხვევები",
|
||||
"memories": "მოგონებები",
|
||||
"memory": "მეხსიერება",
|
||||
"menu": "მენიუ",
|
||||
"merge": "შერწყმა",
|
||||
"minimize": "დაპატარავება"
|
||||
}
|
||||
|
||||
@@ -699,12 +699,18 @@
|
||||
"purchase_button_remove_key": "Noņemt atslēgu",
|
||||
"purchase_button_select": "Izvēlēties",
|
||||
"purchase_individual_description_2": "Atbalstītāja statuss",
|
||||
"purchase_input_suggestion": "Vai tev ir produkta atslēga? Ievadi atslēgu zemāk",
|
||||
"purchase_license_subtitle": "Nopērc Immich licenci, lai atbalstītu turpmāku pakalpojuma attīstību",
|
||||
"purchase_lifetime_description": "Pirkums uz mūžu",
|
||||
"purchase_option_title": "IEGĀDES IESPĒJAS",
|
||||
"purchase_panel_title": "Atbalstīt projektu",
|
||||
"purchase_remove_product_key": "Noņemt produkta atslēgu",
|
||||
"purchase_remove_server_product_key": "Noņemt servera produkta atslēgu",
|
||||
"purchase_server_description_1": "Visam serverim",
|
||||
"purchase_server_description_2": "Atbalstītāja statuss",
|
||||
"purchase_server_title": "Serveris",
|
||||
"purchase_settings_server_activated": "Servera produkta atslēgu pārvalda administrators",
|
||||
"rating_clear": "Noņemt vērtējumu",
|
||||
"reaction_options": "",
|
||||
"read_changelog": "Lasīt izmaiņu sarakstu",
|
||||
"recent": "",
|
||||
|
||||
@@ -372,4 +372,4 @@
|
||||
"yes": "Ya",
|
||||
"you_dont_have_any_shared_links": "Anda tidak mempunyai apa-apa pautan yang dikongsi",
|
||||
"zoom_image": "Zum Gambar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1374,7 +1374,7 @@
|
||||
"welcome": "Velkommen",
|
||||
"welcome_to_immich": "Velkommen til Immich",
|
||||
"year": "År",
|
||||
"years_ago": "{years, plural, one {# year} other {# years}} siden",
|
||||
"years_ago": "{years, plural, one {# år} other {# år}} siden",
|
||||
"yes": "Ja",
|
||||
"you_dont_have_any_shared_links": "Du har ingen delte lenker",
|
||||
"zoom_image": "Zoom Bilde"
|
||||
|
||||
@@ -66,8 +66,13 @@
|
||||
"forcing_refresh_library_files": "Geforceerd vernieuwen van alle bibliotheekbestanden",
|
||||
"image_format": "Formaat",
|
||||
"image_format_description": "WebP produceert kleinere bestanden dan JPEG, maar is langzamer om te verwerken.",
|
||||
"image_fullsize_description": "Afbeelding op ware grootte met gestripte metadata, gebruikt bij inzoomen",
|
||||
"image_fullsize_enabled": "Genereer afbeeldingen op ware grootte inschakelen",
|
||||
"image_fullsize_enabled_description": "Genereer afbeelding op volledig formaat voor niet-webvriendelijke formaten. Als “Ingebed voorvertoning verkiezen” is ingeschakeld, worden ingesloten voorvertoningen direct gebruikt zonder conversie. Heeft geen invloed op webvriendelijke formaten zoals JPEG.",
|
||||
"image_fullsize_quality_description": "Beeldkwaliteit op ware grootte van 1-100. Hoger is beter, maar genereert grotere bestanden.",
|
||||
"image_fullsize_title": "Instellingen afbeelding op ware grootte",
|
||||
"image_prefer_embedded_preview": "Ingebedde voorbeeldafbeelding gebruiken",
|
||||
"image_prefer_embedded_preview_setting_description": "Ingebedde voorbeeldafbeelding van RAW bestanden gebruiken als invoer voor beeldverwerking wanneer beschikbaar. Dit kan preciezere kleuren produceren voor sommige afbeeldingen, maar de kwaliteit van het voorbeeld is afhankelijk van de camera en de afbeelding kan mogelijk meer compressie-artefacten hebben.",
|
||||
"image_prefer_embedded_preview_setting_description": "Ingebedde voorbeeldafbeelding van RAW bestanden gebruiken als invoer voor beeldverwerking wanneer beschikbaar. Dit kan preciezere kleuren produceren voor sommige afbeeldingen, maar de kwaliteit van het voorbeeld is afhankelijk van de camera en de afbeelding kan mogelijk meer compressie-artefacten bevatten.",
|
||||
"image_prefer_wide_gamut": "Voorkeur geven aan wide gamut",
|
||||
"image_prefer_wide_gamut_setting_description": "Display P3 gebruiken voor voorbeeldafbeeldingen. Dit behoudt de levendigheid van afbeeldingen met brede kleurruimtes beter, maar afbeeldingen kunnen er anders uitzien op oude apparaten met een oude browserversie. sRGB-afbeeldingen blijven sRGB gebruiken om kleurverschuivingen te vermijden.",
|
||||
"image_preview_description": "Middelgrote afbeelding met verwijderde metadata, gebruikt bij het bekijken van een enkele asset en voor machine learning",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Video's herhalen",
|
||||
"loop_videos_description": "Inschakelen om video's automatisch te herhalen in de detailweergave.",
|
||||
"main_branch_warning": "U gebruikt een ontwikkelingsversie. Wij raden u ten zeerste aan een releaseversie te gebruiken!",
|
||||
"main_menu": "Hoofdmenu",
|
||||
"make": "Merk",
|
||||
"manage_shared_links": "Beheer gedeelde links",
|
||||
"manage_sharing_with_partners": "Beheer delen met partners",
|
||||
|
||||
@@ -1047,7 +1047,7 @@
|
||||
"purchase_server_title": "Serwer",
|
||||
"purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora",
|
||||
"rating": "Ocena gwiazdkowa",
|
||||
"rating_clear": "Wyczyść oceną",
|
||||
"rating_clear": "Wyczyść ocenę",
|
||||
"rating_count": "{count, plural, one {# gwiazdka} other {# gwiazdek}}",
|
||||
"rating_description": "Wyświetl ocenę z EXIF w panelu informacji",
|
||||
"reaction_options": "Opcje reakcji",
|
||||
@@ -1077,8 +1077,10 @@
|
||||
"remove_custom_date_range": "Usuń niestandardowy zakres dat",
|
||||
"remove_deleted_assets": "Usuń Niedostępne Pliki",
|
||||
"remove_from_album": "Usuń z albumu",
|
||||
"remove_from_favorites": "Usuń z ulubionych",
|
||||
"remove_from_favorites": "Usuń z ulubionych",
|
||||
"remove_from_shared_link": "Usuń z udostępnionego linku",
|
||||
"remove_memory": "Usuń pamięć",
|
||||
"remove_photo_from_memory": "Usuń zdjęcia z tej pamięci",
|
||||
"remove_url": "Usuń URL",
|
||||
"remove_user": "Usuń użytkownika",
|
||||
"removed_api_key": "Usunięto Klucz API: {name}",
|
||||
|
||||
12
i18n/pt.json
12
i18n/pt.json
@@ -66,8 +66,13 @@
|
||||
"forcing_refresh_library_files": "A forçar a atualização de todos os ficheiros da biblioteca",
|
||||
"image_format": "Formato",
|
||||
"image_format_description": "WebP produz ficheiros mais pequenos do que JPEG, mas é mais lento para codificar.",
|
||||
"image_fullsize_description": "Imagem de tamanho inteiro sem meta dados, utilizada quando esta for ampliada",
|
||||
"image_fullsize_enabled": "Ativar geração de imagem em tamanho inteiro",
|
||||
"image_fullsize_enabled_description": "Gerar imagens de tamanho inteiro para formatos não compatíveis com a web. Quando a opção \"Preferir visualização incorporada\" está ativada, estas serão utilizadas diretamente sem serem convertidas. Não afeta formatos compatíveis com a web tais como JPEG.",
|
||||
"image_fullsize_quality_description": "Qualidade da imagem de tamanho inteiro de 1 a 100. Valores mais altos são melhores, mas produzem ficheiros maiores.",
|
||||
"image_fullsize_title": "Definições de imagem de tamanho inteiro",
|
||||
"image_prefer_embedded_preview": "Preferir visualização incorporada",
|
||||
"image_prefer_embedded_preview_setting_description": "Utilizar visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isto pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmara e a imagem pode ter mais artefatos de compressão.",
|
||||
"image_prefer_embedded_preview_setting_description": "Utilizar visualizações incorporadas em fotos RAW como entrada para processamento de imagem e quando disponível. Isto pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmara e a imagem pode ter mais artefatos de compressão.",
|
||||
"image_prefer_wide_gamut": "Prefira ampla gama",
|
||||
"image_prefer_wide_gamut_setting_description": "Utilizar Display P3 para miniaturas. Isso preserva melhor a vibrância das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.",
|
||||
"image_preview_description": "Imagem de tamanho médio sem metadados, utilizada ao visualizar um único ficheiro e pela aprendizagem de máquina",
|
||||
@@ -467,7 +472,7 @@
|
||||
"check_all": "Verificar tudo",
|
||||
"check_logs": "Verificar registos",
|
||||
"choose_matching_people_to_merge": "Escolha pessoas correspondentes para unir",
|
||||
"city": "Cidade",
|
||||
"city": "Cidade/Localidade",
|
||||
"clear": "Limpar",
|
||||
"clear_all": "Limpar tudo",
|
||||
"clear_all_recent_searches": "Limpar todas as pesquisas recentes",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Repetir vídeos",
|
||||
"loop_videos_description": "Ativar para repetir os vídeos automaticamente durante a exibição.",
|
||||
"main_branch_warning": "Está a utilizar uma versão de desenvolvimento, recomendamos vivamente que utilize uma versão estável!",
|
||||
"main_menu": "Menu Principal",
|
||||
"make": "Marca",
|
||||
"manage_shared_links": "Gerir links partilhados",
|
||||
"manage_sharing_with_partners": "Gerir partilha com parceiros",
|
||||
@@ -1244,7 +1250,7 @@
|
||||
"stacktrace": "Stacktrace",
|
||||
"start": "Iniciar",
|
||||
"start_date": "Data de início",
|
||||
"state": "Estado",
|
||||
"state": "Estado/Distrito",
|
||||
"status": "Estado",
|
||||
"stop_motion_photo": "Parar foto em movimento",
|
||||
"stop_photo_sharing": "Deixar de partilhar as suas fotos?",
|
||||
|
||||
@@ -66,8 +66,13 @@
|
||||
"forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca",
|
||||
"image_format": "Formato",
|
||||
"image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.",
|
||||
"image_fullsize_description": "Imagem em tamanho real sem os metadados exibida quando der zoom",
|
||||
"image_fullsize_enabled": "Ativar geração de imagem no tamanho real",
|
||||
"image_fullsize_enabled_description": "Gerar imagens no tamanho real para os formatos de arquivos não compatíveis com a web. Quando \"Preferir visualização incorporada\" estiver ativado, essas serão utilizadas sem conversão. Não afeta arquivos já em formatos para web, como JPEG.",
|
||||
"image_fullsize_quality_description": "Qualidade da imagem em tamanho real, de 1 a 100. Valores maiores tem melhor qualidade, mas gera arquivos maiores.",
|
||||
"image_fullsize_title": "Configurações de imagem em tamanho real",
|
||||
"image_prefer_embedded_preview": "Preferir visualização incorporada",
|
||||
"image_prefer_embedded_preview_setting_description": "Use visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isso pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmera e a imagem pode ter mais artefatos de compactação.",
|
||||
"image_prefer_embedded_preview_setting_description": "Use visualizações incorporadas em fotos RAW como a entrada para processamento de imagem e quando disponível. Isso pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmera e a imagem pode ter mais artefatos de compactação.",
|
||||
"image_prefer_wide_gamut": "Prefira ampla gama",
|
||||
"image_prefer_wide_gamut_setting_description": "Use o Display P3 para miniaturas. Isso preserva melhor a vibração das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.",
|
||||
"image_preview_description": "Imagem de tamanho médio sem os metadados, utilizado quando visualizando um único arquivo e também pelo aprendizado de máquina",
|
||||
@@ -1174,7 +1179,7 @@
|
||||
"server_version": "Versão do servidor",
|
||||
"set": "Definir",
|
||||
"set_as_album_cover": "Definir como capa do álbum",
|
||||
"set_as_featured_photo": "Definir como foto principal",
|
||||
"set_as_featured_photo": "Definir como foto em destaque",
|
||||
"set_as_profile_picture": "Definir como foto de perfil",
|
||||
"set_date_of_birth": "Definir data de nascimento",
|
||||
"set_profile_picture": "Definir foto de perfil",
|
||||
@@ -1187,14 +1192,14 @@
|
||||
"shared_by_user": "Compartilhado por {user}",
|
||||
"shared_by_you": "Compartilhado por você",
|
||||
"shared_from_partner": "Fotos de {partner}",
|
||||
"shared_link_options": "Opções do link compartilhado",
|
||||
"shared_link_options": "Opções de link compartilhado",
|
||||
"shared_links": "Links compartilhados",
|
||||
"shared_links_description": "Compartilhar fotos e videos com um link",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, one {# arquivo compartilhado.} other {# arquivos compartilhados.}}",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, one {# Foto & vídeo compartilhado.} other {# Fotos & vídeos compartilhados.}}",
|
||||
"shared_with_partner": "Compartilhado com {partner}",
|
||||
"sharing": "Compartilhar",
|
||||
"sharing": "Compartilhamento",
|
||||
"sharing_enter_password": "Digite a senha para visualizar esta página.",
|
||||
"sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral",
|
||||
"sharing_sidebar_description": "Exibe um link para Compartilhamento na barra lateral",
|
||||
"shift_to_permanent_delete": "pressione ⇧ para excluir permanentemente o arquivo",
|
||||
"show_album_options": "Exibir opções do álbum",
|
||||
"show_albums": "Exibir álbuns",
|
||||
@@ -1214,15 +1219,15 @@
|
||||
"show_search_options": "Exibir opções de pesquisa",
|
||||
"show_shared_links": "Mostrar links compartilhados",
|
||||
"show_slideshow_transition": "Usar transições no modo de apresentação",
|
||||
"show_supporter_badge": "Insígnia de Contribuidor",
|
||||
"show_supporter_badge_description": "Mostrar a insígnia de contribuidor",
|
||||
"show_supporter_badge": "Insígnia de apoiador",
|
||||
"show_supporter_badge_description": "Mostrar uma insígnia de apoiador",
|
||||
"shuffle": "Aleatório",
|
||||
"sidebar": "Barra lateral",
|
||||
"sidebar_display_description": "Exibir um link para visualizar na barra lateral",
|
||||
"sidebar_display_description": "Exibir um link para a visualização na barra lateral",
|
||||
"sign_out": "Sair",
|
||||
"sign_up": "Registrar",
|
||||
"size": "Tamanho",
|
||||
"skip_to_content": "Pular para o conteúdo",
|
||||
"skip_to_content": "Ir para o conteúdo",
|
||||
"skip_to_folders": "Ir para pastas",
|
||||
"skip_to_tags": "Ir para os marcadores",
|
||||
"slideshow": "Apresentação",
|
||||
@@ -1240,19 +1245,19 @@
|
||||
"stack_duplicates": "Empilhar duplicados",
|
||||
"stack_select_one_photo": "Selecione uma foto principal para a pilha",
|
||||
"stack_selected_photos": "Empilhar fotos selecionadas",
|
||||
"stacked_assets_count": "{count, plural, one {# arquivo empilhado} other {# arquivos empilhados}}",
|
||||
"stacked_assets_count": "{count, plural, one {# Arquivo empilhado} other {# Arquivos empilhados}}",
|
||||
"stacktrace": "Rastreamento de pilha",
|
||||
"start": "Início",
|
||||
"start_date": "Data inicial",
|
||||
"state": "Estado",
|
||||
"status": "Status",
|
||||
"stop_motion_photo": "Parar foto em movimento",
|
||||
"stop_photo_sharing": "Parar de partilhar as suas fotos?",
|
||||
"stop_photo_sharing": "Parar de compartilhar suas fotos?",
|
||||
"stop_photo_sharing_description": "{partner} não terá mais acesso às suas fotos.",
|
||||
"stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este usuário",
|
||||
"stop_sharing_photos_with_user": "Parar de compartilhar suas fotos com este usuário",
|
||||
"storage": "Espaço de armazenamento",
|
||||
"storage_label": "Rótulo de armazenamento",
|
||||
"storage_usage": "utilizado {used} de {available}",
|
||||
"storage_usage": "Utilizado {used} de {available}",
|
||||
"submit": "Enviar",
|
||||
"suggestions": "Sugestões",
|
||||
"sunrise_on_the_beach": "Nascer do sol na praia",
|
||||
@@ -1264,11 +1269,11 @@
|
||||
"tag": "Marcador",
|
||||
"tag_assets": "Marcar arquivos",
|
||||
"tag_created": "Marcador criado: {tag}",
|
||||
"tag_feature_description": "Visualizar fotos e videos agrupados pelo tópico do marcador",
|
||||
"tag_feature_description": "Navegando por fotos e videos agrupados pelo tópico lógico do marcador",
|
||||
"tag_not_found_question": "Não consegue encontrar o marcador? <link>Crie uma novo aqui.</link>",
|
||||
"tag_people": "Marcar pessoas",
|
||||
"tag_updated": "Marcador foi atualizado: {tag}",
|
||||
"tagged_assets": "{count, plural, one {# arquivo marcado} other {# arquivos marcados}}",
|
||||
"tagged_assets": "{count, plural, one {# Arquivo marcado} other {# Arquivos marcados}}",
|
||||
"tags": "Marcadores",
|
||||
"template": "Modelo",
|
||||
"theme": "Tema",
|
||||
@@ -1276,14 +1281,14 @@
|
||||
"theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador",
|
||||
"they_will_be_merged_together": "Eles serão mesclados",
|
||||
"third_party_resources": "Recursos de terceiros",
|
||||
"time_based_memories": "Memórias baseada no tempo",
|
||||
"time_based_memories": "Memórias baseadas no tempo",
|
||||
"timeline": "Linha do tempo",
|
||||
"timezone": "Fuso horário",
|
||||
"to_archive": "Arquivar",
|
||||
"to_change_password": "Alterar senha",
|
||||
"to_favorite": "Favorito",
|
||||
"to_login": "Iniciar sessão",
|
||||
"to_parent": "Voltar um nível acima",
|
||||
"to_parent": "Voltar para nível acima",
|
||||
"to_trash": "Mover para a lixeira",
|
||||
"toggle_settings": "Alternar configurações",
|
||||
"toggle_theme": "Alternar tema escuro",
|
||||
@@ -1297,7 +1302,7 @@
|
||||
"trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira serão deletados permanentemente após {days, plural, one {# dia} other {# dias}}.",
|
||||
"type": "Tipo",
|
||||
"unarchive": "Desarquivar",
|
||||
"unarchived_count": "{count, plural, one {# desarquivado} other {# desarquivados}}",
|
||||
"unarchived_count": "{count, plural, one {# Desarquivado} other {# Desarquivados}}",
|
||||
"unfavorite": "Remover favorito",
|
||||
"unhide_person": "Exibir pessoa",
|
||||
"unknown": "Desconhecido",
|
||||
@@ -1312,19 +1317,19 @@
|
||||
"unnamed_album_delete_confirmation": "Tem certeza que deseja excluir este álbum?",
|
||||
"unnamed_share": "Compartilhamento sem nome",
|
||||
"unsaved_change": "Alteração não salva",
|
||||
"unselect_all": "Limpar seleção",
|
||||
"unselect_all": "Desselecionar todos",
|
||||
"unselect_all_duplicates": "Desselecionar todas as duplicatas",
|
||||
"unstack": "Desempilhar",
|
||||
"unstacked_assets_count": "{count, plural, one {# arquivo não empilhado} other {# arquivos não empilhados}}",
|
||||
"unstacked_assets_count": "{count, plural, one {# Arquivo desempilhado} other {# Arquivos desempilhados}}",
|
||||
"untracked_files": "Arquivos não monitorados",
|
||||
"untracked_files_decription": "Estes arquivos não são monitorados pela aplicação. Podem ser resultados de falhas em uma movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema",
|
||||
"up_next": "A seguir",
|
||||
"updated_password": "Senha atualizada",
|
||||
"upload": "Carregar",
|
||||
"upload_concurrency": "Carregar simultâneo",
|
||||
"upload_concurrency": "Envios simultâneos",
|
||||
"upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos arquivos carregados.",
|
||||
"upload_progress": "{remaining, number} processando - {processed, number}/{total, number} já processados",
|
||||
"upload_skipped_duplicates": "{count, plural, one {# arquivo duplicado foi ignorado} other {# arquivos duplicados foram ignorados}}",
|
||||
"upload_progress": "{remaining, number} restantes - {processed, number}/{total, number} já processados",
|
||||
"upload_skipped_duplicates": "{count, plural, one {# Arquivo duplicado foi ignorado} other {# Arquivos duplicados foram ignorados}}",
|
||||
"upload_status_duplicates": "Duplicados",
|
||||
"upload_status_errors": "Erros",
|
||||
"upload_status_uploaded": "Carregado",
|
||||
@@ -1334,23 +1339,23 @@
|
||||
"use_custom_date_range": "Usar intervalo de datas personalizado",
|
||||
"user": "Usuário",
|
||||
"user_id": "ID do usuário",
|
||||
"user_liked": "{user} curtiu {type, select, photo {a foto} video {o vídeo} asset {o arquivo} other {isso}}",
|
||||
"user_liked": "{user} curtiu {type, select, photo {esta foto} video {este vídeo} asset {este arquivo} other {isto}}",
|
||||
"user_purchase_settings": "Comprar",
|
||||
"user_purchase_settings_description": "Gerenciar sua compra",
|
||||
"user_role_set": "Definir {user} como {role}",
|
||||
"user_usage_detail": "Detalhes de uso do usuário",
|
||||
"user_usage_stats": "Estatísticas de utilização de conta",
|
||||
"user_usage_stats_description": "Ver estatísticas de utilização de conta",
|
||||
"user_usage_stats": "Estatísticas de utilização da conta",
|
||||
"user_usage_stats_description": "Ver estatísticas de utilização da conta",
|
||||
"username": "Nome do usuário",
|
||||
"users": "Usuários",
|
||||
"utilities": "Utilitários",
|
||||
"utilities": "Ferramentas",
|
||||
"validate": "Validar",
|
||||
"variables": "Variáveis",
|
||||
"version": "Versão",
|
||||
"version_announcement_closing": "De seu amigo, Alex",
|
||||
"version_announcement_message": "Olá! Uma nova versão do Immich está disponível. Para evitar configurações incorretas, leia com calma a página de <link>notas da versão</link> e verifique se é necessário alterar alguma configuração, principalmente se você usa o WatchTower ou qualquer outro mecanismo que faça atualizações automáticas do Immich.",
|
||||
"version_history": "Histórico de versões",
|
||||
"version_history_item": "Instalado {version} em {date}",
|
||||
"version_history_item": "Versão {version} instalada em {date}",
|
||||
"video": "Vídeo",
|
||||
"video_hover_setting": "Reproduzir miniatura do vídeo ao passar o mouse",
|
||||
"video_hover_setting_description": "Reproduzir a miniatura do vídeo ao passar o mouse sobre o item. Mesmo quando desativado, a reprodução pode ser iniciada ao passar o mouse sobre o ícone de reprodução.",
|
||||
@@ -1359,15 +1364,15 @@
|
||||
"view": "Ver",
|
||||
"view_album": "Ver álbum",
|
||||
"view_all": "Ver tudo",
|
||||
"view_all_users": "Ver todos usuários",
|
||||
"view_all_users": "Ver todos os usuários",
|
||||
"view_in_timeline": "Ver na linha do tempo",
|
||||
"view_link": "Ver link",
|
||||
"view_links": "Ver links",
|
||||
"view_name": "Ver",
|
||||
"view_next_asset": "Ver próximo arquivo",
|
||||
"view_previous_asset": "Ver arquivo anterior",
|
||||
"view_stack": "Exibir Pilha",
|
||||
"visibility_changed": "A visibilidade de {count, plural, one {# pessoa foi alterada} other {# pessoas foram alteradas}}",
|
||||
"view_stack": "Ver Pilha",
|
||||
"visibility_changed": "A visibilidade {count, plural, one {# da pessoa foi alterada} other {# das pessoas foi alterada}}",
|
||||
"waiting": "Aguardando",
|
||||
"warning": "Aviso",
|
||||
"week": "Semana",
|
||||
@@ -1376,6 +1381,6 @@
|
||||
"year": "Ano",
|
||||
"years_ago": "{years, plural, one {# ano} other {# anos}} atrás",
|
||||
"yes": "Sim",
|
||||
"you_dont_have_any_shared_links": "Não há links compartilhados",
|
||||
"you_dont_have_any_shared_links": "Você não possui links compartilhados",
|
||||
"zoom_image": "Ampliar imagem"
|
||||
}
|
||||
|
||||
@@ -1358,4 +1358,4 @@
|
||||
"yes": "Da",
|
||||
"you_dont_have_any_shared_links": "Nu aveți linkuri partajate",
|
||||
"zoom_image": "Măriți Imaginea"
|
||||
}
|
||||
}
|
||||
|
||||
18
i18n/ru.json
18
i18n/ru.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"about": "О продукте",
|
||||
"account": "Учётная запись",
|
||||
"account_settings": "Настройки аккаунта",
|
||||
"account_settings": "Настройки учётной записи",
|
||||
"acknowledge": "Подтвердить",
|
||||
"action": "Действие",
|
||||
"actions": "Действия",
|
||||
@@ -33,7 +33,7 @@
|
||||
"authentication_settings": "Настройки аутентификации",
|
||||
"authentication_settings_description": "Управление паролями, OAuth и другими настройками аутентификации",
|
||||
"authentication_settings_disable_all": "Вы уверены, что хотите отключить все методы входа? Вход будет полностью отключен.",
|
||||
"authentication_settings_reenable": "Чтобы снова включить, используйте <link>Команда Сервера</link>.",
|
||||
"authentication_settings_reenable": "Чтобы снова включить, используйте <link>Команду сервера</link>.",
|
||||
"background_task_job": "Фоновые задачи",
|
||||
"backup_database": "Резервное копирование базы данных",
|
||||
"backup_database_enable_description": "Включить резервное копирование базы данных",
|
||||
@@ -59,13 +59,18 @@
|
||||
"external_library_created_at": "Внешняя библиотека (создана {date})",
|
||||
"external_library_management": "Управление внешними библиотеками",
|
||||
"face_detection": "Обнаружение лиц",
|
||||
"face_detection_description": "Обнаруживает лица на медиа с помощью машинного обучения. Для видео учитывается только миниатюра. “Обновить” — обработать все медиа. “Сброс” — удалить все имеющиеся данные лиц и обработать заново. “Пропущенные” — добавить в очередь необработанные медиа. Обнаруженные лица будут помещены в очередь распознавания для привязки к существующим или новым людям.",
|
||||
"facial_recognition_job_description": "Группирует распознанные лица по людям. Этот шаг выполняется после завершения обнаружения лиц. “Сброс” - группирует все лица. “Пропущенные” - помещает в очередь лица, не привязанные к человеку.",
|
||||
"face_detection_description": "Обнаруживает лица на медиа с использованием машинного обучения. Для видео анализируется только миниатюра. \"Обновить\" повторно обрабатывает все медиа. \"Сброс\" дополнительно удаляет все имеющиеся данные о лицах. \"Отсутствующие\" ставит в очередь медиа, которые ещё не были обработаны. Обнаруженные лица будут переданы в очередь для распознавания после завершения процесса их обнаружения, привязываясь к существующим или новым людям.",
|
||||
"facial_recognition_job_description": "Группировка обнаруженных лиц по людям. Этот шаг выполняется после завершения обнаружения лиц. \"Сброс\" (пере)группирует все лица. \"Отсутствующие\" ставит в очередь лица, не привязанные к человеку.",
|
||||
"failed_job_command": "Команда {command} не выполнена для задачи: {job}",
|
||||
"force_delete_user_warning": "ПРЕДУПРЕЖДЕНИЕ: Это приведет к немедленному удалению пользователя и его ресурсов. Это действие невозможно отменить, и файлы не могут быть восстановлены.",
|
||||
"forcing_refresh_library_files": "Принудительное обновление всех файлов библиотеки",
|
||||
"image_format": "Формат",
|
||||
"image_format_description": "WebP создает файлы меньшего размера, чем JPEG, но кодирует медленнее.",
|
||||
"image_fullsize_description": "Полноразмерное изображение без метаданных, используется при увеличении",
|
||||
"image_fullsize_enabled": "Включить создание полноразмерного изображения",
|
||||
"image_fullsize_enabled_description": "Создавать полноразмерное изображение для форматов, не предназначенных для веба. Когда включен параметр «Предпочитать встроенное превью», встроенные превью используются напрямую без конверсии. Не влияет на веб-совместимые форматы, такие как JPEG.",
|
||||
"image_fullsize_quality_description": "Качество полноразмерного изображения от 1 до 100. Чем выше значение, тем лучше качество, но больше размер файла.",
|
||||
"image_fullsize_title": "Настройки полноразмерного изображения",
|
||||
"image_prefer_embedded_preview": "Предпочитать встроенное превью",
|
||||
"image_prefer_embedded_preview_setting_description": "Используйте встроенные превью в фотографиях RAW в качестве входных данных для обработки изображений, если они доступны. Это может обеспечить более точную цветопередачу для некоторых изображений, но качество предварительного просмотра зависит от камеры, и изображение может иметь больше артефактов сжатия.",
|
||||
"image_prefer_wide_gamut": "Предпочитаю широкую гамму",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Циклическое воспроизведение",
|
||||
"loop_videos_description": "Включить циклическое воспроизведение видео.",
|
||||
"main_branch_warning": "Вы используете версию для разработки; мы настоятельно рекомендуем использовать релизную версию!",
|
||||
"main_menu": "Главное меню",
|
||||
"make": "Производитель",
|
||||
"manage_shared_links": "Управление публичными ссылками",
|
||||
"manage_sharing_with_partners": "Управление обменом информацией с партнерами. Эта функция позволяет вашему партнеру видеть ваши фотографии и видеозаписи, кроме тех, которые находятся в Архиве и Корзине",
|
||||
@@ -886,7 +892,7 @@
|
||||
"merged_people_count": "Объединено {count, plural, one {# человек} few {# человека} many {# человек} other {# человека}}",
|
||||
"minimize": "Минимизировать",
|
||||
"minute": "Минута",
|
||||
"missing": "Пропущенные",
|
||||
"missing": "Отсутствующие",
|
||||
"model": "Модель",
|
||||
"month": "Месяц",
|
||||
"more": "Больше",
|
||||
@@ -1374,7 +1380,7 @@
|
||||
"welcome": "Добро пожаловать",
|
||||
"welcome_to_immich": "Добро пожаловать в Immich",
|
||||
"year": "Год",
|
||||
"years_ago": "{years, plural, one {# год} few {# года} many {# лет} other {# года}} назад",
|
||||
"years_ago": "{years, plural, one {# год} few {# года} many {# лет} other {# лет}} назад",
|
||||
"yes": "Да",
|
||||
"you_dont_have_any_shared_links": "У вас нет публичных ссылок",
|
||||
"zoom_image": "Приблизить"
|
||||
|
||||
@@ -66,8 +66,13 @@
|
||||
"forcing_refresh_library_files": "Vsiljena osvežitev vseh datotek knjižnice",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP ustvari manjše datoteke kot JPEG, vendar je počasnejši za kodiranje.",
|
||||
"image_fullsize_description": "Slika v polni velikosti brez metapodatkov, uporabljena pri povečavi",
|
||||
"image_fullsize_enabled": "Omogoči ustvarjanje slik v polni velikosti",
|
||||
"image_fullsize_enabled_description": "Ustvari sliko v polni velikosti za formate, ki niso prijazni spletu. Ko je omogočena možnost »Prednostno vdelani predogled«, se vdelani predogledi uporabljajo neposredno brez pretvorbe. Ne vpliva na spletu prijazne formate, kot je JPEG.",
|
||||
"image_fullsize_quality_description": "Kakovost slike v polni velikosti od 1 do 100. Višja vrednost pomeni boljšo kakovost, vendar ustvarja večje datoteke.",
|
||||
"image_fullsize_title": "Nastavitve slike v polni velikosti",
|
||||
"image_prefer_embedded_preview": "Uporabi raje vdelan predogled",
|
||||
"image_prefer_embedded_preview_setting_description": "Uporabite vdelane predoglede v fotografije RAW kot vhod za obdelavo slik, ko so na voljo. To lahko ustvari natančnejše barve za nekatere slike, vendar je kakovost predogleda odvisna od kamere in slika ima lahko več artefaktov stiskanja.",
|
||||
"image_prefer_embedded_preview_setting_description": "Uporabi vdelane predoglede v fotografijah RAW kot vhod za obdelavo slik, kadar so na voljo. To lahko pri nekaterih slikah zagotovi natančnejše barve, vendar je kakovost predogleda odvisna od fotoaparata, slika pa lahko vsebuje več artefaktov stiskanja.",
|
||||
"image_prefer_wide_gamut": "Uporabi raje širok razpon",
|
||||
"image_prefer_wide_gamut_setting_description": "Uporabite P3 Display za sličice. To bolje ohranja živahnost slik s širokimi barvnimi prostori, vendar so lahko slike videti drugače na starih napravah s staro različico brskalnika. Slike sRGB se ohranijo kot sRGB, da se izognejo barvnim zamikom.",
|
||||
"image_preview_description": "Slika srednje velikosti z odstranjenimi metapodatki, ki se uporablja pri ogledu posameznega sredstva in za strojno učenje",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Zanka videoposnetkov",
|
||||
"loop_videos_description": "Omogočite samodejno ponavljanje videoposnetka v pregledovalniku podrobnosti.",
|
||||
"main_branch_warning": "Uporabljate razvojno različico; močno priporočamo uporabo izdajne različice!",
|
||||
"main_menu": "Glavni meni",
|
||||
"make": "Izdelava",
|
||||
"manage_shared_links": "Upravljanje povezav v skupni rabi",
|
||||
"manage_sharing_with_partners": "Upravljajte skupno rabo s partnerji",
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Принудно освежавање свих датотека библиотеке",
|
||||
"image_format": "Формат",
|
||||
"image_format_description": "WebP производи мање датотеке од ЈПЕГ, али се спорије кодира.",
|
||||
"image_fullsize_description": "Слика у пуној величини са огољеним метаподацима, користи се када је увећана",
|
||||
"image_fullsize_enabled": "Омогућите генерисање слике у пуној величини",
|
||||
"image_fullsize_enabled_description": "Генеришите слику пуне величине за формате који нису прилагођени вебу. Када је „Преферирај уграђени преглед“ омогућен, уграђени прегледи се користе директно без конверзије. Не утиче на формате прилагођене вебу као што је JPEG.",
|
||||
"image_fullsize_quality_description": "Квалитет слике у пуној величини од 1-100. Више је боље, али производи веће датотеке.",
|
||||
"image_fullsize_title": "Подешавања слике у пуној величини",
|
||||
"image_prefer_embedded_preview": "Преферирајте уграђени преглед",
|
||||
"image_prefer_embedded_preview_setting_description": "Користите уграђене прегледе у RAW фотографије као улаз за обраду слике када су доступне. Ово може да произведе прецизније боје за неке слике, али квалитет прегледа зависи од камере и слика може имати више неправилности компресије.",
|
||||
"image_prefer_wide_gamut": "Преферирајте широк спектар",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Понављајте видео записе",
|
||||
"loop_videos_description": "Омогућите за аутоматско понављање видео записа у прегледнику детаља.",
|
||||
"main_branch_warning": "Употребљавате развојну верзију; строго препоручујемо употребу издате верзије!",
|
||||
"main_menu": "Главни мени",
|
||||
"make": "Креирај",
|
||||
"manage_shared_links": "Управљајте дељеним везама",
|
||||
"manage_sharing_with_partners": "Управљајте дељењем са партнерима",
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Prinudno osvežavanje svih datoteka biblioteke",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP proizvodi manje datoteke od JPEG, ali se sporije kodira.",
|
||||
"image_fullsize_description": "Slika u punoj veličini sa ogoljenim metapodacima, koristi se kada je uvećana",
|
||||
"image_fullsize_enabled": "Omogućite generisanje slike u punoj veličini",
|
||||
"image_fullsize_enabled_description": "Generišite sliku pune veličine za formate koji nisu prilagođeni vebu. Kada je „Preferiraj ugrađeni pregled“ omogućen, ugrađeni pregledi se koriste direktno bez konverzije. Ne utiče na formate prilagođene vebu kao što je JPEG.",
|
||||
"image_fullsize_quality_description": "Kvalitet slike u punoj veličini od 1-100. Više je bolje, ali proizvodi veće datoteke.",
|
||||
"image_fullsize_title": "Podešavanja slike u punoj veličini",
|
||||
"image_prefer_embedded_preview": "Preferirajte ugrađeni pregled",
|
||||
"image_prefer_embedded_preview_setting_description": "Koristite ugrađene preglede u RAW fotografije kao ulaz za obradu slike kada su dostupne. Ovo može da proizvede preciznije boje za neke slike, ali kvalitet pregleda zavisi od kamere i slika može imati više nepravilnosti kompresije.",
|
||||
"image_prefer_wide_gamut": "Preferirajte širok spektar",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Ponavljajte video zapise",
|
||||
"loop_videos_description": "Omogućite za automatsko ponavljanje video zapisa u pregledniku detalja.",
|
||||
"main_branch_warning": "Upotrebljavate razvojnu verziju; strogo preporučujemo upotrebu izdate verzije!",
|
||||
"main_menu": "Glavni meni",
|
||||
"make": "Kreiraj",
|
||||
"manage_shared_links": "Upravljajte deljenim vezama",
|
||||
"manage_sharing_with_partners": "Upravljajte deljenjem sa partnerima",
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "Tvingar uppdatering av alla biblioteksfiler",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP producerar mindre filer än JPEG, men kodas långsammare.",
|
||||
"image_fullsize_description": "Fullstor bild med borttagen metadata, används vid inzoomning",
|
||||
"image_fullsize_enabled": "Använd fullstor bildgenerering",
|
||||
"image_fullsize_enabled_description": "Generera fullstor bild för icke webbvänliga format. När \"Använd inbäddade förhandsvisningar\" är aktiverat används inbäddad förhandsvisning utan konvertering. Påverkar inte webbvänliga format som JPEG.",
|
||||
"image_fullsize_quality_description": "Bildkvalitet för fullstora bilder 1-100. Högre värde ger bättre kvalitet men större filer.",
|
||||
"image_fullsize_title": "Inställningar för fullstora bilder",
|
||||
"image_prefer_embedded_preview": "Föredra inbäddad förhandsgranskning",
|
||||
"image_prefer_embedded_preview_setting_description": "Använd inbäddade förhandsvisningar i RAW-foton som indata till bildbehandling när det är tillgängligt. Detta kan ge mer exakta färger för vissa bilder, men kvaliteten på förhandsgranskningen är kameraberoende och bilden kan ha fler komprimeringsartefakter.",
|
||||
"image_prefer_wide_gamut": "Föredrar brett spektrum",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Loopa videor",
|
||||
"loop_videos_description": "Aktivera för att automatiskt loopa en video i detaljvisaren.",
|
||||
"main_branch_warning": "Du använder en utvecklingsversion. Vi rekommenderar starkt att du använder en utgiven version!",
|
||||
"main_menu": "Huvudmeny",
|
||||
"make": "Tillverkare",
|
||||
"manage_shared_links": "Hantera Delade länkar",
|
||||
"manage_sharing_with_partners": "Hantera delning med partner",
|
||||
|
||||
@@ -1339,4 +1339,4 @@
|
||||
"yes": "ஆம்",
|
||||
"you_dont_have_any_shared_links": "உங்களிடம் பகிரப்பட்ட இணைப்புகள் எதுவும் இல்லை",
|
||||
"zoom_image": "பெரிதாக்க படம்"
|
||||
}
|
||||
}
|
||||
|
||||
10
i18n/te.json
10
i18n/te.json
@@ -58,7 +58,7 @@
|
||||
"exclusion_pattern_description": "మినహాయింపు నమూనాలు మీ లైబ్రరీని స్కాన్ చేస్తున్నప్పుడు ఫైల్లు మరియు ఫోల్డర్లను విస్మరించడానికి మిమ్మల్ని అనుమతిస్తాయి. మీరు దిగుమతి చేయకూడదనుకునే RAW ఫైల్లు వంటి ఫోల్డర్లను కలిగి ఉన్నట్లయితే ఇది ఉపయోగకరంగా ఉంటుంది.",
|
||||
"external_library_created_at": "బాహ్య లైబ్రరీ ({date}న సృష్టించబడింది)",
|
||||
"external_library_management": "బాహ్య లైబ్రరీ నిర్వహణ",
|
||||
"face_detection": "ముఖ గుర్తింపు",
|
||||
"face_detection": "ముఖ గమనింపు",
|
||||
"face_detection_description": "మెషిన్ లెర్నింగ్ ఉపయోగించి ఆస్తులలో ముఖాలను గుర్తించండి. వీడియోల కోసం, సూక్ష్మచిత్రం మాత్రమే పరిగణించబడుతుంది. \"అన్నీ\" (పునః) అన్ని ఆస్తులను ప్రాసెస్ చేస్తుంది. ఇంకా ప్రాసెస్ చేయని ఆస్తులను \"మిస్సింగ్\" క్యూలు చేస్తుంది. గుర్తించబడిన ముఖాలు ఇప్పటికే ఉన్న లేదా కొత్త వ్యక్తులతో సమూహపరచడం పూర్తయిన తర్వాత ముఖ గుర్తింపు కోసం క్యూలో ఉంచబడతాయి.",
|
||||
"facial_recognition_job_description": "సమూహం వ్యక్తుల ముఖాలను గుర్తించింది. ఫేస్ డిటెక్షన్ పూర్తయిన తర్వాత ఈ దశ అమలవుతుంది. \"అన్ని\" (పునః) అన్ని ముఖాలను క్లస్టర్లు చేస్తుంది. \"తప్పిపోయిన\" వ్యక్తిని కేటాయించని ముఖాలను క్యూలో ఉంచుతుంది.",
|
||||
"failed_job_command": "ఉద్యోగం కోసం కమాండ్ {command} విఫలమైంది: {job}",
|
||||
@@ -67,7 +67,7 @@
|
||||
"image_format": "ఫార్మాట్",
|
||||
"image_format_description": "WebP JPEG కంటే చిన్న ఫైల్లను ఉత్పత్తి చేస్తుంది, కానీ ఎన్కోడ్ చేయడం నెమ్మదిగా ఉంటుంది.",
|
||||
"image_prefer_embedded_preview": "పొందుపరిచిన పరిదృశ్యానికి ప్రాధాన్యత ఇవ్వండి",
|
||||
"image_prefer_embedded_preview_setting_description": "అందుబాటులో ఉన్నప్పుడు ఇమేజ్ ప్రాసెసింగ్కు ఇన్పుట్గా RAW ఫోటోలలో ఎంబెడెడ్ ప్రివ్యూలను ఉపయోగించండి. ఇది కొన్ని చిత్రాలకు మరింత ఖచ్చితమైన రంగులను ఉత్పత్తి చేయగలదు, అయితే ప్రివ్యూ నాణ్యత కెమెరాపై ఆధారపడి ఉంటుంది మరియు చిత్రం మరిన్ని కుదింపు కళాఖండాలను కలిగి ఉండవచ్చు.",
|
||||
"image_prefer_embedded_preview_setting_description": "అందుబాటులో ఉన్నప్పుడు మరియు ఇమేజ్ ప్రాసెసింగ్కు ఇన్పుట్గా RAW ఫోటోలలో ఎంబెడెడ్ ప్రివ్యూలను ఉపయోగించండి. ఇది కొన్ని చిత్రాలకు మరింత ఖచ్చితమైన రంగులను ఉత్పత్తి చేయగలదు, అయితే ప్రివ్యూ నాణ్యత కెమెరాపై ఆధారపడి ఉంటుంది మరియు చిత్రం మరిన్ని కుదింపు కళాఖండాలను కలిగి ఉండవచ్చు.",
|
||||
"image_prefer_wide_gamut": "విస్తృత స్వరసప్తకానికి ప్రాధాన్యత ఇవ్వండి",
|
||||
"image_prefer_wide_gamut_setting_description": "థంబ్నెయిల్ల కోసం డిస్ప్లే P3ని ఉపయోగించండి. ఇది విస్తృత రంగుల ఖాళీలతో చిత్రాల వైబ్రెన్స్ను మెరుగ్గా భద్రపరుస్తుంది, అయితే పాత బ్రౌజర్ వెర్షన్తో పాత పరికరాల్లో చిత్రాలు విభిన్నంగా కనిపించవచ్చు. రంగు మార్పులను నివారించడానికి sRGB చిత్రాలు sRGB వలె ఉంచబడతాయి.",
|
||||
"image_preview_description": "ఒకే ఆస్తిని వీక్షించేటప్పుడు మరియు యంత్ర అభ్యాసం కోసం మెటాడేటా లేని మధ్యస్థ-పరిమాణ చిత్రం ఉపయోగించబడుతుంది",
|
||||
@@ -113,7 +113,7 @@
|
||||
"machine_learning_enabled": "మెషిన్ లెర్నింగ్ ప్రారంభించండి",
|
||||
"machine_learning_enabled_description": "డిజేబుల్ చేయబడితే, దిగువ సెట్టింగ్లతో సంబంధం లేకుండా అన్ని ML ఫీచర్లు నిలిపివేయబడతాయి.",
|
||||
"machine_learning_facial_recognition": "ముఖ గుర్తింపు",
|
||||
"machine_learning_facial_recognition_description": "చిత్రాలలో ముఖాలను గుర్తించండి, గుర్తించండి మరియు సమూహపరచండి",
|
||||
"machine_learning_facial_recognition_description": "చిత్రాలలో ముఖాలను కనుగొనండి, గుర్తించండి మరియు సమూహపరచండి",
|
||||
"machine_learning_facial_recognition_model": "ముఖ గుర్తింపు మోడల్",
|
||||
"machine_learning_facial_recognition_model_description": "నమూనాలు పరిమాణం యొక్క అవరోహణ క్రమంలో జాబితా చేయబడ్డాయి. పెద్ద మోడల్లు నెమ్మదిగా ఉంటాయి మరియు ఎక్కువ మెమరీని ఉపయోగిస్తాయి, కానీ మంచి ఫలితాలను ఇస్తాయి. మీరు మోడల్ను మార్చిన తర్వాత అన్ని చిత్రాల కోసం తప్పనిసరిగా ఫేస్ డిటెక్షన్ జాబ్ని మళ్లీ అమలు చేయాలని గుర్తుంచుకోండి.",
|
||||
"machine_learning_facial_recognition_setting": "ముఖ గుర్తింపును ప్రారంభించండి",
|
||||
@@ -395,7 +395,7 @@
|
||||
"allow_public_user_to_download": "పబ్లిక్ వినియోగదారుడు డౌన్లోడ్ చేసేందుకు అనుమతించండి",
|
||||
"allow_public_user_to_upload": "పబ్లిక్ వినియోగదారుడు అప్లోడ్ చేసేందుకు అనుమతించండి",
|
||||
"alt_text_qr_code": "క్యూఆర్ కోడ్ చిత్రం",
|
||||
"anti_clockwise": "ఎడమవైపు తిరిగే దిశ",
|
||||
"anti_clockwise": "అపసవ్య-దిశ",
|
||||
"api_key": "API కీ",
|
||||
"api_key_description": "ఈ విలువ ఒక్కసారి మాత్రమే చూపబడుతుంది. విండోను మూసివేసే ముందు దయచేసి దీనిని ఖచ్చితంగా కాపీ చేసి ఎక్కడైనా భద్రపరచండి.",
|
||||
"api_key_empty": "మీ API కీ పేరు ఖాళీగా ఉండకూడదు",
|
||||
@@ -473,7 +473,7 @@
|
||||
"clear_all_recent_searches": "ఇటీవల చేసిన అన్ని శోధనలను ఖాళీ చేయి",
|
||||
"clear_message": "సందేశాన్ని ఖాళీ చేయి",
|
||||
"clear_value": "విలువను ఖాళీ చేయి",
|
||||
"clockwise": "సమయదిశగా",
|
||||
"clockwise": "సవ్యదిశ",
|
||||
"close": "మూసివేయి",
|
||||
"collapse": "సంకుచితం చేయి",
|
||||
"collapse_all": "అన్నీ సంకుచితం చేయి",
|
||||
|
||||
67
i18n/th.json
67
i18n/th.json
@@ -41,6 +41,7 @@
|
||||
"backup_settings": "ตั้งค่าการสำรองข้อมูล",
|
||||
"backup_settings_description": "จัดการการตั้งค่าการสำรองฐานข้อมูล",
|
||||
"check_all": "ตรวจสอบทั้งหมด",
|
||||
"cleanup": "ทำความสะอาด",
|
||||
"cleared_jobs": "เคลียร์งานสำหรับ: {job}",
|
||||
"config_set_by_file": "การตั้งค่าคอนฟิกกำลังถูกกำหนดโดยไฟล์คอนฟิก",
|
||||
"confirm_delete_library": "คุณแน่ใจว่าอยากลบคลังภาพ {library} หรือไม่?",
|
||||
@@ -65,8 +66,12 @@
|
||||
"forcing_refresh_library_files": "บังคับรีเฟรชไฟล์ทั้งหมด",
|
||||
"image_format": "Format",
|
||||
"image_format_description": "WebP จะให้ไฟล์ที่เล็กกว่า JPEG แต่ใช้เวลาแปลงไฟล์นานกว่า",
|
||||
"image_fullsize_description": "รูปภาพขนาดเต็มที่ถูกถอดข้อมูล metadata ออก ใช้ในขณะทำการขยายรูปภาพดู",
|
||||
"image_fullsize_enabled": "เปิดใช้งานการสร้างรูปภาพขนาดเต็ม",
|
||||
"image_fullsize_quality_description": "คุณภาพรูปภาพขนาดเต็มจาก 1-100 ค่ายิ่งสูงคุณภาพยิ่งสูง แต่แลกมาด้วยขนาดไฟล์ที่ใหญ่ขึ้น",
|
||||
"image_fullsize_title": "ตั้งค่ารูปภาพขนาดเต็ม",
|
||||
"image_prefer_embedded_preview": "ใช้พรีวิวแบบฝังตัว",
|
||||
"image_prefer_embedded_preview_setting_description": "ใช้พรีวิวฝังตัวในรูปภาพ RAW ในการวิเคราะห์รูปภาพถ้ามี แต่คุณภาพรูปภาพขึ้นอยู่กับกล้อง และอาจจะมีสิ่งตกค้างจากการย่อขนาดไฟล์",
|
||||
"image_prefer_embedded_preview_setting_description": "ใช้การแสดงภาพแบบฝังตัวในรูปภาพ RAW ในการวิเคราะห์รูปภาพหากสามารถใช้ได้ สิ่งนี้จะช่วยให้รูปภาพมีสีสันที่ถูกต้องมากยิ่งขึ้น แต่อย่างไรก็ตาม คุณภาพรูปภาพขึ้นอยู่กับกล้องถ่ายรูป และอาจจะเกิดร่องรอย ลาย ที่ไม่พึงประสงค์บนรูปภาพ จากการย่อขนาดไฟล์",
|
||||
"image_prefer_wide_gamut": "ใช้ช่วงสีกว้าง",
|
||||
"image_prefer_wide_gamut_setting_description": "ใช้ Display P3 สำหรับภาพตัวอย่าง (thumbnails) เพื่อรักษาความสดใสของภาพที่มีช่วงสีที่กว้างขึ้น อย่างไรก็ตาม ภาพอาจแสดงผลแตกต่างกันบนอุปกรณ์เก่าที่ใช้เว็บเบราว์เซอร์เวอร์ชันเก่า สำหรับภาพที่อยู่ใน sRGB จะยังคงใช้ sRGB ต่อไปเพื่อหลีกเลี่ยงการเปลี่ยนแปลงของสี",
|
||||
"image_preview_description": "ภาพขนาดปานกลางที่ถูกลบข้อมูลเมตา ใช้สำหรับการดูแอสเซ็ตเดี่ยวและสำหรับการเรียนรู้ของเครื่อง (Machine Learning)",
|
||||
@@ -131,7 +136,7 @@
|
||||
"machine_learning_smart_search_description": "ค้นหาภาพโดยใช้ความหมายจากการใช้ CLIP",
|
||||
"machine_learning_smart_search_enabled": "เปิดใช้งานการค้นหาอัจฉริยะ",
|
||||
"machine_learning_smart_search_enabled_description": "หากปิดใช้งาน ภาพจะไม่ถูกใช้สําหรับการค้นหาอัจฉริยะ",
|
||||
"machine_learning_url_description": "URL ของเซิร์ฟเวอร์ machine learning",
|
||||
"machine_learning_url_description": "URL ของเซิร์ฟเวอร์ machine learning กรณีมี URL มากกว่าหนึ่ง URL จะทำการทดลองส่งข้อมูลเรียงไปทีละอันตามลำดับจนกว่าจะพบ URL ที่ตอบสนอง และจะเลิกส่งข้อมูลชั่วคราวในส่วนของ URL ที่ไม่ตอบสนอง",
|
||||
"manage_concurrency": "จัดการการทำงานพร้อมกัน",
|
||||
"manage_log_settings": "จัดการการตั้งค่าจดบันทึก",
|
||||
"map_dark_style": "แบบมืด",
|
||||
@@ -147,6 +152,8 @@
|
||||
"map_settings": "การตั้งค่าแผนที่และ GPS",
|
||||
"map_settings_description": "จัดการการตั้งค่าแผนที่",
|
||||
"map_style_description": "URL ไปยังธีมแผนที่ style.json",
|
||||
"memory_cleanup_job": "ล้างข้อมูลในหน่วยความจำ (memory)",
|
||||
"memory_generate_job": "การสร้างความทรงจำ",
|
||||
"metadata_extraction_job": "ดึงข้อมูล metadata",
|
||||
"metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS และความคมชัด",
|
||||
"metadata_faces_import_setting": "เปิดการนำเข้าข้อมูลใบหน้า",
|
||||
@@ -219,7 +226,7 @@
|
||||
"reset_settings_to_default": "ตั้งค่าการตั้งค่าเป็นค่าเริ่มต้น",
|
||||
"reset_settings_to_recent_saved": "ตั้งค่าการตั้งค่าเป็นค่าล่าสุด",
|
||||
"scanning_library": "กำลังสแกนคลัง",
|
||||
"search_jobs": "ค้นหางาน",
|
||||
"search_jobs": "ค้นหางาน…",
|
||||
"send_welcome_email": "ส่งอีเมลต้อนรับ",
|
||||
"server_external_domain_settings": "โดเมนภายนอก",
|
||||
"server_external_domain_settings_description": "โดเมนสำหรับลิงก์แชร์สาธารณะ แบบมี http(s)://",
|
||||
@@ -240,7 +247,7 @@
|
||||
"storage_template_hash_verification_enabled_description": "เปิดใช้งานการตรวจสอบ hash ห้ามปิดใช้งานเว้นแต่คุณจะเข้าใจผลกระทบ",
|
||||
"storage_template_migration": "การย้ายเทมเพลตที่เก็บข้อมูล",
|
||||
"storage_template_migration_description": "ใช้<link>{template}</link>ปัจจุบันกับสื่อที่อัปโหลดก่อนหน้านี้",
|
||||
"storage_template_migration_info": "การเปลี่ยนแปลงเท็มเพลตจะมีผลกับแอสเซ็ตใหม่เท่านั้น หากต้องการนำเทมเพลตไปใช้กับ Asset ที่อัปโหลดก่อนหน้านี้ ให้รัน <link>{job}</link>.",
|
||||
"storage_template_migration_info": "เทมเพลตของการจัดเก็บข้อมูลจะเปลี่ยนตัวอักษรเป็นตัวพิมพ์เล็กทั้งหมด การเปลี่ยนแปลงเทมเพลตจะมีผลกับแอสเซ็ตใหม่เท่านั้น หากต้องการนำเทมเพลตไปใช้กับ Asset ที่อัปโหลดก่อนหน้านี้ ให้รัน <link>{job}</link>.",
|
||||
"storage_template_migration_job": "เทมเพลตการ Migration ข้อมูล",
|
||||
"storage_template_more_details": "สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับฟีเจอร์นี้ โปรดดูที่ <template-link>Storage Template</template-link> และ <implications-link>ผลกระทบ</implications-link>",
|
||||
"storage_template_onboarding_description": "เมื่อเปิดใช้งาน ฟีเจอร์นี้จะจัดระเบียบไฟล์โดยอัตโนมัติตามเทมเพลตที่ผู้ใช้กำหนด เนื่องจากปัญหาด้านความเสถียร ฟีเจอร์นี้จึงถูกปิดใช้งานเป็นค่าเริ่มต้น สำหรับข้อมูลเพิ่มเติม โปรดดูที่ <link>เอกสารประกอบ</link>",
|
||||
@@ -313,7 +320,7 @@
|
||||
"transcoding_reference_frames_description": "จำนวนเฟรมที่จะอ้างอิงเมื่อบีบอัดเฟรมที่กำหนด ค่าที่สูงขึ้นจะช่วยเพิ่มประสิทธิภาพในการบีบอัด แต่จะทำให้การเข้ารหัสช้าลง ค่า 0 จะตั้งค่านี้โดยอัตโนมัติ",
|
||||
"transcoding_required_description": "เฉพาะวิดีโอที่ไม่อยู่ในรูปแบบที่ยอมรับเท่านั้น",
|
||||
"transcoding_settings": "การตั้งค่าการแปลงไฟล์วิดีโอ",
|
||||
"transcoding_settings_description": "จัดการข้อมูลความคมชัดและแบบไฟล์วิดีโอ",
|
||||
"transcoding_settings_description": "จัดการว่าวีดีโอไหนจะถูกแปลงการเข้ารหัส (Transcode) และวิธีการประมวลผลไฟล์ดังกล่าว",
|
||||
"transcoding_target_resolution": "เป้าหมายความคมชัด",
|
||||
"transcoding_target_resolution_description": "ความคมชัดที่สูงกว่าจะเก็บรายละเอียดดีกว่าแต่ใช้เวลาแปลงไฟล์นานกว่า ขนาดไฟล์ใหญ่กว่า และลดการตอบสนองของแอป",
|
||||
"transcoding_temporal_aq": "AQ ชั่วคราว",
|
||||
@@ -353,6 +360,7 @@
|
||||
"version_check_implications": "การตรวจสอบเวอร์ชันใหม่จะต้องติดต่อกับ github.com เป็นระยะ",
|
||||
"version_check_settings": "ตรวจสอบรุ่น",
|
||||
"version_check_settings_description": "เปิด/ปิดการแจ้งเตือนรุ่นใหม่",
|
||||
"video_conversion_job": "เข้ารหัสวีดีโอ (transcode)",
|
||||
"video_conversion_job_description": "แปลงไฟล์วิดีโอเพึ่อรองรับบราวเซอร์และเครื่องเล่นอื่น ๆ มากขึ้น"
|
||||
},
|
||||
"admin_email": "อีเมลผู้ดูแลระบบ",
|
||||
@@ -369,7 +377,7 @@
|
||||
"album_delete_confirmation_description": "หากแชร์อัลบั้มนี้ ผู้ใช้รายอื่นจะไม่สามารถเข้าถึงได้อีก",
|
||||
"album_info_updated": "อัปเดทข้อมูลอัลบั้มแล้ว",
|
||||
"album_leave": "ออกจากอัลบั้ม ?",
|
||||
"album_leave_confirmation": "คุณต้องการออกจากอัลบั้ม {album} ใช่หรือไม่",
|
||||
"album_leave_confirmation": "คุณต้องการออกจากอัลบั้ม {album} ใช่หรือไม่?",
|
||||
"album_name": "ชื่ออัลบั้ม",
|
||||
"album_options": "ตัวเลือกอัลบั้ม",
|
||||
"album_remove_user": "ลบผู้ใช้ ?",
|
||||
@@ -390,6 +398,7 @@
|
||||
"allow_edits": "อนุญาตให้แก้ไขได้",
|
||||
"allow_public_user_to_download": "อนุญาตให้ผู้ใช้สาธารณะดาวน์โหลดได้",
|
||||
"allow_public_user_to_upload": "อนุญาตให้ผู้ใช้สาธารณะอัปโหลดได้",
|
||||
"alt_text_qr_code": "รูปภาพ QR code",
|
||||
"anti_clockwise": "ทวนเข็มนาฬิกา",
|
||||
"api_key": "API key",
|
||||
"api_key_description": "ค่านี้จะแสดงเพียงครั้งเดียว โปรดคัดลอกก่อนปิดหน้าต่าง",
|
||||
@@ -444,7 +453,7 @@
|
||||
"cancel": "ยกเลิก",
|
||||
"cancel_search": "ยกเลิกการค้นหา",
|
||||
"cannot_merge_people": "ไม่สามารถรวมกลุ่มคนได้",
|
||||
"cannot_undo_this_action": "ไม่สามารถย้อนกลับได้",
|
||||
"cannot_undo_this_action": "การกระทำนี้ไม่สามารถย้อนกลับได้!",
|
||||
"cannot_update_the_description": "ไม่สามารถอัพเดทรายละเอียดได้",
|
||||
"change_date": "เปลี่ยนวันที่",
|
||||
"change_expiration_time": "เปลี่ยนเวลาหมดอายุ",
|
||||
@@ -476,6 +485,7 @@
|
||||
"comments_are_disabled": "ความคิดเห็นถูกปิดใช้งาน",
|
||||
"confirm": "ยืนยัน",
|
||||
"confirm_admin_password": "ยืนยันรหัสผ่านผู้ดูแลระบบ",
|
||||
"confirm_delete_face": "คุณแน่ใจว่าต้องการลบใบหน้า{name}ออกหรือไม่?",
|
||||
"confirm_delete_shared_link": "คุณต้องการที่จะลบลิงก์ที่แชร์ใช่หรือไม่ ?",
|
||||
"confirm_keep_this_delete_others": "จะลบทั้งหมดในรายการ และยกเว้นสื่อนี้หรือไม่ คุณแน่ใจใช่ไหมที่ต้องการดำเนินการต่อ?",
|
||||
"confirm_password": "ยืนยันรหัสผ่าน",
|
||||
@@ -528,13 +538,14 @@
|
||||
"delete_album": "ลบอัลบั้ม",
|
||||
"delete_api_key_prompt": "คุณต้องการลบ API คีย์ นี้ใช่ไหม ?",
|
||||
"delete_duplicates_confirmation": "คุณแน่ใจที่ต้องการลบรายการซ้ำอย่างถาวรใช่ไหม ?",
|
||||
"delete_face": "ลบใบหน้า",
|
||||
"delete_key": "ลบกุญแจ",
|
||||
"delete_library": "ลบคลังภาพ",
|
||||
"delete_link": "ลบลิงก์",
|
||||
"delete_others": "ลบผู้อื่น",
|
||||
"delete_shared_link": "ลบลิงก์ที่แชร์",
|
||||
"delete_tag": "ลบแท็ก",
|
||||
"delete_tag_confirmation_prompt": "คุณต้องการลบแท็ก {tagName} ใช่หรือไม่",
|
||||
"delete_tag_confirmation_prompt": "คุณแน่ใจว่าต้องการลบแท็ก {tagName} ใช่หรือไม่?",
|
||||
"delete_user": "ลบผู้ใช้",
|
||||
"deleted_shared_link": "ลบลิงก์ที่แชร์แล้ว",
|
||||
"deletes_missing_assets": "ลบสื่อที่หายไปออกจากดิสถ์",
|
||||
@@ -543,7 +554,7 @@
|
||||
"direction": "เส้นทาง",
|
||||
"disabled": "ปิดการใช้งาน",
|
||||
"disallow_edits": "ไม่อนุญาตให้แก้ไข",
|
||||
"discord": "Discord",
|
||||
"discord": "ดิสคอร์ด",
|
||||
"discover": "ค้นพบ",
|
||||
"dismiss_all_errors": "ปฏิเสธข้อผิดพลาดทั้งหมด",
|
||||
"dismiss_error": "ปฏิเสธข้อผิดพลาด",
|
||||
@@ -595,6 +606,7 @@
|
||||
"enabled": "เปิดใช้งาน",
|
||||
"end_date": "วันสิ้นสุด",
|
||||
"error": "เกิดข้อผิดพลาด",
|
||||
"error_delete_face": "เกิดเออเรอร์ ไม่สามารถลบใบหน้าออกได้",
|
||||
"error_loading_image": "เกิดข้อผิดพลาดระหว่างโหลดภาพ",
|
||||
"error_title": "เกิดข้อผิดพลาด",
|
||||
"errors": {
|
||||
@@ -613,7 +625,7 @@
|
||||
"error_adding_users_to_album": "เกิดข้อผิดพลาดในการเพิ่มผู้ใช้ไปยังอัลบั้ม",
|
||||
"error_deleting_shared_user": "เกิดข้อผิดพลาดในการลบผู้ใช้ที่แชร์",
|
||||
"error_downloading": "ไม่สามารถดาวน์โหลด {filename} ได้",
|
||||
"error_hiding_buy_button": "Error hiding buy button",
|
||||
"error_hiding_buy_button": "เกิดข้อผิดพลาด ไม่สามารถซ่อนปุ่มซื้อได้",
|
||||
"error_removing_assets_from_album": "เกิดข้อผิดพลาดในการลบสื่อจากอัลบั้ม",
|
||||
"error_selecting_all_assets": "เกิดข้อผิดพลาดในการเลือกสื่อทั้งหมด",
|
||||
"exclusion_pattern_already_exists": "ข้อยกเว้นนี้มีอยู่แล้ว",
|
||||
@@ -626,7 +638,7 @@
|
||||
"failed_to_load_asset": "ไม่สามารถโหลดสื่อได้",
|
||||
"failed_to_load_assets": "ไม่สามารถโหลดสื่อได้",
|
||||
"failed_to_load_people": "ไม่สามารถโหลดบุคคลได้",
|
||||
"failed_to_remove_product_key": "Failed to remove product key",
|
||||
"failed_to_remove_product_key": "ไม่สามารถลบ product key ได้",
|
||||
"failed_to_stack_assets": "Failed to stack assets",
|
||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||
"import_path_already_exists": "พาธนำเข้านี้มีอยู่แล้ว",
|
||||
@@ -760,8 +772,10 @@
|
||||
"go_to_folder": "ไปที่โฟล์เดอร์",
|
||||
"go_to_search": "กลับไปยังการค้นหา",
|
||||
"group_albums_by": "จัดกลุ่มอัลบั้มตาม",
|
||||
"group_country": "จัดเรียงกลุ่มตามประเทศ",
|
||||
"group_no": "ไม่จัดกลุ่ม",
|
||||
"group_owner": "จัดกลุ่มโดยเจ้าของ",
|
||||
"group_places_by": "จัดเรียงกลุ่มของสถานที่ด้วยการ...",
|
||||
"group_year": "จัดกลุ่มตามปี",
|
||||
"has_quota": "เหลือพื้นที่",
|
||||
"hi_user": "สวัสดีคุณ {name} {email}",
|
||||
@@ -842,6 +856,7 @@
|
||||
"loop_videos": "วนวิดีโอ",
|
||||
"loop_videos_description": "เปิดเพื่อให้วิดีโอวนลูปในที่ดูรายละเอียด",
|
||||
"main_branch_warning": "คุณกำลังใช้เวอร์ชันการพัฒนา เราขอแนะนำอย่างยิ่งให้ใช้เวอร์ชันเสถียร !",
|
||||
"main_menu": "เมนูหลัก",
|
||||
"make": "สร้าง",
|
||||
"manage_shared_links": "จัดการลิงก์ที่แชร์",
|
||||
"manage_sharing_with_partners": "จัดการการแชร์กับคู่หู",
|
||||
@@ -969,6 +984,7 @@
|
||||
"permanently_deleted_asset": "ลบสื่อถาวรแล้ว",
|
||||
"permanently_deleted_assets_count": "ลบ {count, plural, one {# asset} other {# assets}} เรียบร้อยแล้ว",
|
||||
"person": "บุคคล",
|
||||
"person_birthdate": "เกิดวัน{date}",
|
||||
"photo_shared_all_users": "ดูเหมือนว่าคุณได้แชร์รูปภาพของคุณกับผู้ใช้ทั้งหมด หรือคุณไม่มีผู้ใช้ใดที่จะแชร์ด้วย",
|
||||
"photos": "รูปภาพ",
|
||||
"photos_and_videos": "รูปภาพ และ วิดีโอ",
|
||||
@@ -1058,12 +1074,16 @@
|
||||
"remove_from_album": "ลบออกจากอัลบั้ม",
|
||||
"remove_from_favorites": "เอาออกจากรายการโปรด",
|
||||
"remove_from_shared_link": "ลบออกจากลิงก์ที่แชร์",
|
||||
"remove_memory": "ลบความทรงจำ",
|
||||
"remove_photo_from_memory": "ลบรูปออกจากความทรงจำนี้",
|
||||
"remove_url": "ลบ URL",
|
||||
"remove_user": "ลบผู้ใช้",
|
||||
"removed_api_key": "API คีย์ของ: {name} ถูกลบแล้ว",
|
||||
"removed_from_archive": "ลบจากเก็บถาวรแล้ว",
|
||||
"removed_from_favorites": "ลบจากรายการโปรดแล้ว",
|
||||
"removed_from_favorites_count": "{count, plural, other {ถูกลบ#}} จากรายการโปรดแล้ว",
|
||||
"removed_memory": "ความทรงจำที่ถูกลบ",
|
||||
"removed_photo_from_memory": "รูปที่ถูกลบออกจากความทรงจำ",
|
||||
"removed_tagged_assets": "ลบแท็กจาก {count, plural, one {# สื่อ} other {# สื่อ}}",
|
||||
"rename": "เปลี่ยนชื่อ",
|
||||
"repair": "ซ่อมแซม",
|
||||
@@ -1071,6 +1091,7 @@
|
||||
"replace_with_upload": "อัปโหลดทับรูปหรือวิดีโอนี้",
|
||||
"require_password": "ต้องการรหัสผ่าน",
|
||||
"require_user_to_change_password_on_first_login": "จำเป็นต้องเปลี่ยนรหัสผ่าน ในการเข้าสู่ระบบครั้งแรก",
|
||||
"rescan": "สแกนใหม่",
|
||||
"reset": "รีเซ็ต",
|
||||
"reset_password": "ตั้งค่ารหัสผ่านใหม่",
|
||||
"reset_people_visibility": "ปรับการมองเห็นใหม่",
|
||||
@@ -1099,6 +1120,8 @@
|
||||
"search": "ค้นหา",
|
||||
"search_albums": "ค้นหาอัลบั้ม",
|
||||
"search_by_context": "ค้นหาตามบริบท",
|
||||
"search_by_description": "ค้นหาด้วยคำอธิบาย",
|
||||
"search_by_description_example": "วันปีนเขาในซาปา",
|
||||
"search_by_filename": "ค้นหาชื่อไฟล์ชื่อ หรือ ชนิดไฟล์",
|
||||
"search_by_filename_example": "ตัวอย่าง. IMG_1234.JPG หรือ PNG",
|
||||
"search_camera_make": "ค้นหายี่ห้อกล้อง",
|
||||
@@ -1112,6 +1135,7 @@
|
||||
"search_options": "ตัวเลือกการค้นหา",
|
||||
"search_people": "ค้นหาผู้คน",
|
||||
"search_places": "ค้นหาสถานที่",
|
||||
"search_rating": "ค้นหาตามเรตติ้ง...",
|
||||
"search_settings": "ตั้งค่าการค้นหา",
|
||||
"search_state": "ค้นหาตามรัฐ",
|
||||
"search_tags": "ค้นหาแท็ก",
|
||||
@@ -1121,6 +1145,7 @@
|
||||
"searching_locales": "ค้นหาตามภูมิภาค",
|
||||
"second": "วินาที",
|
||||
"see_all_people": "ดูบุคคลทั้งหมด",
|
||||
"select": "เลือก",
|
||||
"select_album_cover": "เลือกภาพปกอัลบั้ม",
|
||||
"select_all": "เลือกทั้งหมด",
|
||||
"select_all_duplicates": "เลือกรายการที่ซ้ำทั้งหมด",
|
||||
@@ -1158,6 +1183,7 @@
|
||||
"shared_from_partner": "รูปจาก {partner}",
|
||||
"shared_link_options": "ตั้งค่าลิงก์ที่แชร์",
|
||||
"shared_links": "ลิงก์ที่แชร์",
|
||||
"shared_links_description": "แบ่งปันรูปและวีดีโอด้วยลิ้งค์",
|
||||
"shared_with_partner": "แชร์กับ {partner}",
|
||||
"sharing": "การแชร์",
|
||||
"sharing_enter_password": "โปรดป้อนรหัสผ่าน สำหรับเปิดดูหน้านี้",
|
||||
@@ -1179,6 +1205,7 @@
|
||||
"show_person_options": "แสดงตัวเลือกของตัวบุคคล",
|
||||
"show_progress_bar": "แสดงความคืบหน้า แถบ",
|
||||
"show_search_options": "แสดงตัวเลือกการค้นหา",
|
||||
"show_shared_links": "แสดงลิ้งค์ที่ถูกแบ่งปัน",
|
||||
"show_slideshow_transition": "แสดงสไลค์โชว์",
|
||||
"show_supporter_badge": "เครื่องหมายผู้สนับสนุน",
|
||||
"show_supporter_badge_description": "แสดงเครื่องหมายผู้สนับสนุน",
|
||||
@@ -1203,17 +1230,21 @@
|
||||
"sort_title": "ไตเติ้ล",
|
||||
"source": "แหล่ง",
|
||||
"stack": "ซ้อน",
|
||||
"stack_selected_photos": "",
|
||||
"stack_duplicates": "นำสิ่งที่ซ้ำมาซ้อนอยู่ด้วยกัน",
|
||||
"stack_select_one_photo": "เลือกรูปหลักหนึ่งรูปสำหรับรูปที่ซ้อนกันนี้",
|
||||
"stack_selected_photos": "ซ้อนรูปที่ถูกเลือก",
|
||||
"stacktrace": "",
|
||||
"start": "เริ่มต้น",
|
||||
"start_date": "วันที่เริ่ม",
|
||||
"state": "รัฐ",
|
||||
"status": "สถานะ",
|
||||
"stop_motion_photo": "ภาพวัตถุเคลื่อนไหว",
|
||||
"stop_photo_sharing": "หยุดแชร์รูปภาพ?",
|
||||
"stop_photo_sharing_description": "{partner}จะไม่สามารถเข้าถึงรูปของคุณได้อีก",
|
||||
"stop_sharing_photos_with_user": "หยุดการแชร์รูปภาพของคุณกับผู้ใช้นี้",
|
||||
"storage": "พื้นที่จัดเก็บ",
|
||||
"storage_label": "เนื้อที่จัดเก็บ",
|
||||
"storage_usage": "ใช้ไป {used} จาก {available} ",
|
||||
"storage_usage": "ใช้ไป {used} จาก {available}",
|
||||
"submit": "ส่ง",
|
||||
"suggestions": "ข้อเสนอแนะ",
|
||||
"sunrise_on_the_beach": "พระอาทิตย์ขึ้นบนชายหาด",
|
||||
@@ -1224,22 +1255,28 @@
|
||||
"sync": "ซิงค์",
|
||||
"tag": "แท็ก",
|
||||
"tag_created": "สร้างแท็ก: {tag}",
|
||||
"tag_not_found_question": "ไม่สามารถหาแท็กได้ใช่หรือไม่?<link>สร้างแท็กใหม่</link>",
|
||||
"tag_people": "แท็กผู้คน",
|
||||
"tag_updated": "แท็กที่ถูกอัพเดต: {tag}",
|
||||
"tags": "แท็ก",
|
||||
"template": "เท็มเพลต",
|
||||
"theme": "ธีม",
|
||||
"theme_selection": "การเลือกธีม",
|
||||
"theme_selection_description": "ตั้งค่าธีมให้สว่างหรือมืดโดยอัตโนมัติ อิงจากค่าของเบราว์เซอร์ของคุณ",
|
||||
"they_will_be_merged_together": "จะถูกรวมเข้าด้วยกัน",
|
||||
"third_party_resources": "ทรัพยากรบุคคลที่สาม",
|
||||
"time_based_memories": "ความทรงจําตามเวลา",
|
||||
"timeline": "Timeline",
|
||||
"timeline": "ทามไลน์",
|
||||
"timezone": "เขตเวลา",
|
||||
"to_archive": "จัดเก็บถาวร",
|
||||
"to_change_password": "Change password",
|
||||
"to_change_password": "เปลี่ยนรหัสผ่าน",
|
||||
"to_favorite": "รายการโปรด",
|
||||
"to_login": "เข้าสู่ระบบ",
|
||||
"to_parent": "ไปยังบนสุด",
|
||||
"to_trash": "ถังขยะ",
|
||||
"toggle_settings": "สลับการตั้งค่า",
|
||||
"toggle_theme": "สลับธีม",
|
||||
"total": "ทั้งหมด",
|
||||
"total_usage": "การใช้งานรวม",
|
||||
"trash": "ถังขยะ",
|
||||
"trash_all": "ทิ้งทั้งหมด",
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
"machine_learning_smart_search_description": "Fotoğrafları CLIP kullanarak semantik olarak ara",
|
||||
"machine_learning_smart_search_enabled": "Akıllı aramayı etkinleştir",
|
||||
"machine_learning_smart_search_enabled_description": "Eğer devre dışı bırakılırsa fotoğraflar akıllı arama için işlenmeyecek.",
|
||||
"machine_learning_url_description": "Makine öğrenimi sunucusunun URL'si. Birden fazla URL sağlanırsa, ilkinden sonuna doğru, biri başarılı bir şekilde yanıt verene kadar her sunucu tek tek denenir.",
|
||||
"machine_learning_url_description": "Makine öğrenimi sunucusunun URL’si. Birden fazla URL sağlanırsa, her sunucu sırayla tek tek denenir ve biri başarılı yanıt verene kadar devam edilir. Yanıt vermeyen sunucular, çevrimiçi duruma gelene kadar geçici olarak yok sayılır.",
|
||||
"manage_concurrency": "Aynı anda çalışmayı yönet",
|
||||
"manage_log_settings": "Günlük ayarlarını yönet",
|
||||
"map_dark_style": "Koyu mod",
|
||||
|
||||
18
i18n/uk.json
18
i18n/uk.json
@@ -66,18 +66,23 @@
|
||||
"forcing_refresh_library_files": "Примусове оновлення всіх файлів бібліотеки",
|
||||
"image_format": "Формат",
|
||||
"image_format_description": "Формат WebP виробляє меньші файлів, ніж JPEG, але його кодування вимагає більше часу.",
|
||||
"image_prefer_embedded_preview": "Надати перевагу вбудованому перегляду",
|
||||
"image_prefer_embedded_preview_setting_description": "Використовуйте вбудовані попередні перегляди у RAW фотографіях як вхідні дані для обробки зображень, коли це можливо. Це може забезпечити більш точні кольори для деяких зображень, але якість попереднього перегляду залежить від камери та зображення можуть мати більше артефактів стиснення.",
|
||||
"image_fullsize_description": "Повнорозмірне зображення з видаленими метаданими, які використовуються під час збільшення",
|
||||
"image_fullsize_enabled": "Увімкнути створення повнорозмірного зображення",
|
||||
"image_fullsize_enabled_description": "Генерувати зображення повного розміру для форматів, не призначених для вебу. Якщо увімкнено \"Надавати перевагу вбудованому прев’ю\", вбудовані прев’ю використовуються без конвертації. Не впливає на веб-дружні формати, такі як JPEG.",
|
||||
"image_fullsize_quality_description": "Якість повнорозмірного зображення від 1 до 100. Чим вище значення, тим краще якість, але більше розмір файлу.",
|
||||
"image_fullsize_title": "Налаштування повнорозмірного зображення",
|
||||
"image_prefer_embedded_preview": "Надавати перевагу вбудованому прев’ю",
|
||||
"image_prefer_embedded_preview_setting_description": "Використовувати вбудовані прев’ю в RAW-фотографіях як вхідні дані для обробки зображень, якщо вони доступні. Це може забезпечити точніші кольори для деяких зображень, але якість прев’ю залежить від камери і зображення може містити більше артефактів стиснення.",
|
||||
"image_prefer_wide_gamut": "Віддають перевагу широкій гамі",
|
||||
"image_prefer_wide_gamut_setting_description": "Для мініатюр використовуйте дисплей P3. Це краще зберігає яскравість зображень з широким колірним простором, але на старих пристроях зі старою версією браузера зображення можуть виглядати інакше. sRGB-зображення зберігаються у форматі sRGB, щоб уникнути зсуву кольорів.",
|
||||
"image_preview_description": "Зображення середнього розміру з видаленими метаданими, яке використовується при перегляді одного об'єкта та для машинного навчання",
|
||||
"image_preview_quality_description": "Якість попереднього перегляду від 1 до 100. Вища оцінка означає кращу якість, але створює більші файли та може зменшити швидкість роботи програми. Встановлення низького значення може вплинути на якість машинного навчання.",
|
||||
"image_preview_title": "Налаштування попереднього перегляду",
|
||||
"image_preview_quality_description": "Якість прев’ю від 1 до 100. Вища оцінка означає кращу якість, але створює більші файли та може зменшити швидкість роботи програми. Встановлення низького значення може вплинути на якість машинного навчання.",
|
||||
"image_preview_title": "Налаштування прев’ю",
|
||||
"image_quality": "Якість",
|
||||
"image_resolution": "Роздільність",
|
||||
"image_resolution_description": "Вища роздільність може зберігати більше деталей, але займає більше часу для кодування, має більші розміри файлів і може зменшити швидкість роботи програми.",
|
||||
"image_settings": "Налаштування зображення",
|
||||
"image_settings_description": "Керуйте якістю та роздільною здатністю згенерованих зображень",
|
||||
"image_settings_description": "Керувати якістю та роздільною здатністю згенерованих зображень",
|
||||
"image_thumbnail_description": "Маленька мініатюра із видаленими метаданими, що використовується для перегляду груп фотографій, наприклад, на основній лінії часу",
|
||||
"image_thumbnail_quality_description": "Якість мініатюри від 1 до 100. Вища оцінка означає кращу якість, але створює більші файли та може зменшити швидкість роботи програми.",
|
||||
"image_thumbnail_title": "Налаштування мініатюр",
|
||||
@@ -256,7 +261,7 @@
|
||||
"template_email_available_tags": "Ви можете використовувати наступні змінні у вашому шаблоні: {tags}",
|
||||
"template_email_if_empty": "Якщо шаблон порожній, буде використано стандартний ел. лист.",
|
||||
"template_email_invite_album": "Шаблон запрошення до альбому",
|
||||
"template_email_preview": "Попередній перегляд",
|
||||
"template_email_preview": "Прев’ю",
|
||||
"template_email_settings": "Шаблони ел. листів",
|
||||
"template_email_settings_description": "Керувати шаблонами сповіщень ел. пошти",
|
||||
"template_email_update_album": "Оновити шаблон альбому",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "Циклічні відео",
|
||||
"loop_videos_description": "Увімкнути циклічне відтворення відео.",
|
||||
"main_branch_warning": "Ви використовуєте версію для розробників; ми настійно рекомендуємо використовувати релізну версію!",
|
||||
"main_menu": "Головне меню",
|
||||
"make": "Виробник",
|
||||
"manage_shared_links": "Керування спільними посиланнями",
|
||||
"manage_sharing_with_partners": "Керуйте спільним використанням з партнерами",
|
||||
|
||||
@@ -1317,4 +1317,4 @@
|
||||
"yes": "Có",
|
||||
"you_dont_have_any_shared_links": "Bạn không có liên kết chia sẻ nào",
|
||||
"zoom_image": "Thu phóng ảnh"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"about": "關於",
|
||||
"account": "帳號",
|
||||
"account_settings": "帳號設定",
|
||||
"acknowledge": "明白",
|
||||
"acknowledge": "了解",
|
||||
"action": "操作",
|
||||
"actions": "操作",
|
||||
"actions": "進行動作",
|
||||
"active": "處理中",
|
||||
"activity": "動態",
|
||||
"activity_changed": "動態已{enabled, select, true {啟用} other {停用}}",
|
||||
"activity_changed": "動態{enabled, select, true {開啟} other {關閉}}",
|
||||
"add": "加入",
|
||||
"add_a_description": "加入文字說明",
|
||||
"add_a_location": "新增地點",
|
||||
@@ -23,8 +23,8 @@
|
||||
"add_to": "加入到…",
|
||||
"add_to_album": "加入到相簿",
|
||||
"add_to_shared_album": "加到共享相簿",
|
||||
"add_url": "新增URL",
|
||||
"added_to_archive": "已新增至封存",
|
||||
"add_url": "建立連結",
|
||||
"added_to_archive": "移至封存",
|
||||
"added_to_favorites": "加入收藏",
|
||||
"added_to_favorites_count": "將 {count, number} 個項目加入收藏",
|
||||
"admin": {
|
||||
@@ -33,7 +33,7 @@
|
||||
"authentication_settings": "驗證設定",
|
||||
"authentication_settings_description": "管理密碼、OAuth 與其他驗證設定",
|
||||
"authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入。",
|
||||
"authentication_settings_reenable": "如需重新啟用,請使用 <link>伺服器指令</link>。",
|
||||
"authentication_settings_reenable": "如需重新啟用,請使用 <link> 伺服器指令 </link> 。",
|
||||
"background_task_job": "背景執行",
|
||||
"backup_database": "備份資料庫",
|
||||
"backup_database_enable_description": "啟用資料庫備份",
|
||||
@@ -485,7 +485,7 @@
|
||||
"comments_are_disabled": "留言已停用",
|
||||
"confirm": "確認",
|
||||
"confirm_admin_password": "確認管理者密碼",
|
||||
"confirm_delete_face": "您確定要從資產中刪除 {name} 的臉部嗎?",
|
||||
"confirm_delete_face": "您確定要從項目中刪除 {name} 的臉孔嗎?",
|
||||
"confirm_delete_shared_link": "確定刪除連結嗎?",
|
||||
"confirm_keep_this_delete_others": "所有的其他堆疊項目將被刪除。確定繼續嗎?",
|
||||
"confirm_password": "確認密碼",
|
||||
@@ -606,7 +606,7 @@
|
||||
"enabled": "己啟用",
|
||||
"end_date": "結束日期",
|
||||
"error": "錯誤",
|
||||
"error_delete_face": "從資產中刪除臉部時發生錯誤",
|
||||
"error_delete_face": "從項目中刪除臉孔時發生錯誤",
|
||||
"error_loading_image": "載入圖片時出錯",
|
||||
"error_title": "錯誤 - 出問題了",
|
||||
"errors": {
|
||||
@@ -618,7 +618,7 @@
|
||||
"cant_change_metadata_assets_count": "無法更改 {count, plural, other {# 個檔案}}的詳細資料",
|
||||
"cant_get_faces": "無法取得臉孔",
|
||||
"cant_get_number_of_comments": "無法取得留言數量",
|
||||
"cant_search_people": "無法搜尋人",
|
||||
"cant_search_people": "未搜尋到人物",
|
||||
"cant_search_places": "無法搜尋地點",
|
||||
"cleared_jobs": "已清除的作業:{job}",
|
||||
"error_adding_assets_to_album": "將檔案加入相簿時出錯",
|
||||
@@ -1374,4 +1374,4 @@
|
||||
"yes": "是",
|
||||
"you_dont_have_any_shared_links": "您沒有任何共享連結",
|
||||
"zoom_image": "縮放圖片"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"forcing_refresh_library_files": "强制刷新所有图库文件",
|
||||
"image_format": "格式",
|
||||
"image_format_description": "WebP 文件体积较 JPEG 文件更小,但编码速度较慢。",
|
||||
"image_fullsize_description": "去除元数据的全尺寸图像,放大时使用",
|
||||
"image_fullsize_enabled": "启用全尺寸图像生成",
|
||||
"image_fullsize_enabled_description": "生成非网络友好格式的全尺寸图像。启用 “首选嵌入式预览 ”后,将直接使用嵌入式预览而无需转换。不影响 JPEG 等网络友好格式。",
|
||||
"image_fullsize_quality_description": "全尺寸图像质量从 1 到 100。越高越好,但生成的文件较大。",
|
||||
"image_fullsize_title": "全尺寸图像设置",
|
||||
"image_prefer_embedded_preview": "嵌入式预览",
|
||||
"image_prefer_embedded_preview_setting_description": "优先使用 RAW 照片的嵌入式预览作为图像处理的输入。可以提升某些影像的颜色准确度,但嵌入式预览的质量取决于相机,图像可能压缩失真更严重。",
|
||||
"image_prefer_wide_gamut": "广色域",
|
||||
@@ -859,6 +864,7 @@
|
||||
"loop_videos": "循环视频",
|
||||
"loop_videos_description": "启用在详细信息中自动循环播放视频。",
|
||||
"main_branch_warning": "您当前使用的是开发版;我们强烈建议您使用正式发行版(release版)!",
|
||||
"main_menu": "主菜单",
|
||||
"make": "品牌",
|
||||
"manage_shared_links": "管理共享链接",
|
||||
"manage_sharing_with_partners": "管理与同伴的共享",
|
||||
|
||||
@@ -61,6 +61,7 @@ custom_lint:
|
||||
# refactor to make the providers and services testable
|
||||
- lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
|
||||
- lib/services/{background,backup}.service.dart # uses only PMProgressHandler
|
||||
- test/**.dart
|
||||
- import_rule_isar:
|
||||
message: isar must only be used in entities and repositories
|
||||
restrict: package:isar
|
||||
@@ -150,7 +151,6 @@ dart_code_metrics:
|
||||
- avoid-unnecessary-continue
|
||||
- avoid-unnecessary-nullable-return-type: false
|
||||
- binary-expression-operand-order
|
||||
- move-variable-outside-iteration
|
||||
- pattern-fields-ordering
|
||||
- prefer-abstract-final-static-class
|
||||
- prefer-commenting-future-delayed
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
@@ -124,4 +125,4 @@
|
||||
<data android:scheme="geo" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import java.security.MessageDigest
|
||||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `BackgroundService`
|
||||
*
|
||||
* Receives messages/method calls from the foreground Dart side to manage
|
||||
* the background service, e.g. start (enqueue), stop (cancel)
|
||||
* Android plugin for Dart `BackgroundService` and file trash operations
|
||||
*/
|
||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
|
||||
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var fileTrashChannel: MethodChannel? = null
|
||||
private var context: Context? = null
|
||||
private var pendingResult: Result? = null
|
||||
private val PERMISSION_REQUEST_CODE = 1001
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
@@ -29,6 +44,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
context = ctx
|
||||
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
|
||||
methodChannel?.setMethodCallHandler(this)
|
||||
|
||||
// Add file trash channel
|
||||
fileTrashChannel = MethodChannel(messenger, "file_trash")
|
||||
fileTrashChannel?.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
@@ -38,11 +57,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private fun onDetachedFromEngine() {
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
fileTrashChannel?.setMethodCallHandler(null)
|
||||
fileTrashChannel = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
val ctx = context!!
|
||||
when (call.method) {
|
||||
// Existing BackgroundService methods
|
||||
"enable" -> {
|
||||
val args = call.arguments<ArrayList<*>>()!!
|
||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
@@ -114,10 +136,180 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// File Trash methods moved from MainActivity
|
||||
"moveToTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
if (fileName != null) {
|
||||
if (hasManageStoragePermission()) {
|
||||
val success = moveToTrash(fileName)
|
||||
result.success(success)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Storage permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
"restoreFromTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
if (fileName != null) {
|
||||
if (hasManageStoragePermission()) {
|
||||
val success = untrashImage(fileName)
|
||||
result.success(success)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Storage permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
"requestManageStoragePermission" -> {
|
||||
if (!hasManageStoragePermission()) {
|
||||
requestManageStoragePermission(result)
|
||||
} else {
|
||||
Log.e("Manage storage permission", "Permission already granted")
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// File Trash methods moved from MainActivity
|
||||
private fun hasManageStoragePermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestManageStoragePermission(result: Result) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pendingResult = result // Store the result callback
|
||||
val activity = activityBinding?.activity ?: return
|
||||
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.data = Uri.parse("package:${activity.packageName}")
|
||||
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToTrash(fileName: String): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
val uri = getFileUri(fileName)
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
return uri?.let { moveToTrash(it) } ?: false
|
||||
}
|
||||
|
||||
private fun moveToTrash(contentUri: Uri): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
return try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.IS_TRASHED, 1) // Move to trash
|
||||
}
|
||||
val updated = contentResolver.update(contentUri, values, null, null)
|
||||
updated > 0
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error moving to trash", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileUri(fileName: String): Uri? {
|
||||
val contentResolver = context?.contentResolver ?: return null
|
||||
val contentUri = MediaStore.Files.getContentUri("external")
|
||||
val projection = arrayOf(MediaStore.Images.Media._ID)
|
||||
val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?"
|
||||
val selectionArgs = arrayOf(fileName)
|
||||
var fileUri: Uri? = null
|
||||
|
||||
contentResolver.query(contentUri, projection, selection, selectionArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
|
||||
fileUri = ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
return fileUri
|
||||
}
|
||||
|
||||
private fun untrashImage(name: String): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
val uri = getTrashedFileUri(contentResolver, name)
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
return uri?.let { untrashImage(it) } ?: false
|
||||
}
|
||||
|
||||
private fun untrashImage(contentUri: Uri): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
return try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.IS_TRASHED, 0) // Restore file
|
||||
}
|
||||
val updated = contentResolver.update(contentUri, values, null, null)
|
||||
updated > 0
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error restoring file", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrashedFileUri(contentResolver: ContentResolver, fileName: String): Uri? {
|
||||
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
|
||||
|
||||
val queryArgs = Bundle().apply {
|
||||
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?")
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||
}
|
||||
|
||||
contentResolver.query(contentUri, projection, queryArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
||||
return ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ActivityAware implementation
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
// ActivityResultListener implementation
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
val granted = hasManageStoragePermission()
|
||||
pendingResult?.success(granted)
|
||||
pendingResult = null
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val BUFFER_SIZE = 2 * 1024 * 1024;
|
||||
private const val BUFFER_SIZE = 2 * 1024 * 1024
|
||||
|
||||
@@ -2,14 +2,12 @@ package app.alextran.immich
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||
// No need to set up method channel here as it's now handled in the plugin
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,10 +17,14 @@
|
||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
|
||||
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
|
||||
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
||||
"advanced_settings_tile_title": "Advanced",
|
||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||
"advanced_settings_sync_remote_deletions_title": "Sync remote deletions [EXPERIMENTAL]",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web",
|
||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||
"album_info_card_backup_album_included": "INCLUDED",
|
||||
"albums": "Albums",
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -685,7 +685,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -715,7 +715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -748,7 +748,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -791,7 +791,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -831,7 +831,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 201;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.131.0</string>
|
||||
<string>1.131.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -93,7 +93,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>200</string>
|
||||
<string>201</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -4,3 +4,6 @@ const double downloadFailed = -2;
|
||||
|
||||
// Number of log entries to retain on app start
|
||||
const int kLogTruncateLimit = 250;
|
||||
|
||||
const int kBatchHashFileLimit = 128;
|
||||
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
12
mobile/lib/domain/interfaces/device_asset.interface.dart
Normal file
12
mobile/lib/domain/interfaces/device_asset.interface.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
||||
|
||||
abstract interface class IDeviceAssetRepository implements IDatabaseRepository {
|
||||
Future<bool> updateAll(List<DeviceAsset> assetHash);
|
||||
|
||||
Future<List<DeviceAsset>> getByIds(List<String> localIds);
|
||||
|
||||
Future<void> deleteIds(List<String> ids);
|
||||
}
|
||||
44
mobile/lib/domain/models/device_asset.model.dart
Normal file
44
mobile/lib/domain/models/device_asset.model.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
class DeviceAsset {
|
||||
final String assetId;
|
||||
final Uint8List hash;
|
||||
final DateTime modifiedTime;
|
||||
|
||||
const DeviceAsset({
|
||||
required this.assetId,
|
||||
required this.hash,
|
||||
required this.modifiedTime,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant DeviceAsset other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.assetId == assetId &&
|
||||
other.hash == hash &&
|
||||
other.modifiedTime == modifiedTime;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)';
|
||||
}
|
||||
|
||||
DeviceAsset copyWith({
|
||||
String? assetId,
|
||||
Uint8List? hash,
|
||||
DateTime? modifiedTime,
|
||||
}) {
|
||||
return DeviceAsset(
|
||||
assetId: assetId ?? this.assetId,
|
||||
hash: hash ?? this.hash,
|
||||
modifiedTime: modifiedTime ?? this.modifiedTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,10 @@ enum StoreKey<T> {
|
||||
|
||||
// Video settings
|
||||
loadOriginalVideo<bool>._(136),
|
||||
;
|
||||
manageLocalMediaAndroid<bool>._(137),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000);
|
||||
|
||||
const StoreKey._(this.id);
|
||||
final int id;
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -358,7 +359,7 @@ class Asset {
|
||||
// take most values from newer asset
|
||||
// keep vales that can never be set by the asset not in DB
|
||||
if (a.isRemote) {
|
||||
return a._copyWith(
|
||||
return a.copyWith(
|
||||
id: id,
|
||||
localId: localId,
|
||||
width: a.width ?? width,
|
||||
@@ -366,7 +367,7 @@ class Asset {
|
||||
exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo,
|
||||
);
|
||||
} else if (isRemote) {
|
||||
return _copyWith(
|
||||
return copyWith(
|
||||
localId: localId ?? a.localId,
|
||||
width: width ?? a.width,
|
||||
height: height ?? a.height,
|
||||
@@ -374,7 +375,7 @@ class Asset {
|
||||
);
|
||||
} else {
|
||||
// TODO: Revisit this and remove all bool field assignments
|
||||
return a._copyWith(
|
||||
return a.copyWith(
|
||||
id: id,
|
||||
remoteId: remoteId,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
@@ -394,7 +395,7 @@ class Asset {
|
||||
// fill in potentially missing values, i.e. merge assets
|
||||
if (a.isRemote) {
|
||||
// values from remote take precedence
|
||||
return _copyWith(
|
||||
return copyWith(
|
||||
remoteId: a.remoteId,
|
||||
width: a.width,
|
||||
height: a.height,
|
||||
@@ -416,7 +417,7 @@ class Asset {
|
||||
);
|
||||
} else {
|
||||
// add only missing values (and set isLocal to true)
|
||||
return _copyWith(
|
||||
return copyWith(
|
||||
localId: localId ?? a.localId,
|
||||
width: width ?? a.width,
|
||||
height: height ?? a.height,
|
||||
@@ -427,7 +428,7 @@ class Asset {
|
||||
}
|
||||
}
|
||||
|
||||
Asset _copyWith({
|
||||
Asset copyWith({
|
||||
Id? id,
|
||||
String? checksum,
|
||||
String? remoteId,
|
||||
@@ -488,6 +489,9 @@ class Asset {
|
||||
|
||||
static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
|
||||
|
||||
static int compareByLocalId(Asset a, Asset b) =>
|
||||
compareToNullable(a.localId, b.localId);
|
||||
|
||||
static int compareByChecksum(Asset a, Asset b) =>
|
||||
a.checksum.compareTo(b.checksum);
|
||||
|
||||
|
||||
36
mobile/lib/infrastructure/entities/device_asset.entity.dart
Normal file
36
mobile/lib/infrastructure/entities/device_asset.entity.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'device_asset.entity.g.dart';
|
||||
|
||||
@Collection(inheritance: false)
|
||||
class DeviceAssetEntity {
|
||||
Id get id => fastHash(assetId);
|
||||
|
||||
@Index(replace: true, unique: true, type: IndexType.hash)
|
||||
final String assetId;
|
||||
@Index(unique: false, type: IndexType.hash)
|
||||
final List<byte> hash;
|
||||
final DateTime modifiedTime;
|
||||
|
||||
const DeviceAssetEntity({
|
||||
required this.assetId,
|
||||
required this.hash,
|
||||
required this.modifiedTime,
|
||||
});
|
||||
|
||||
DeviceAsset toModel() => DeviceAsset(
|
||||
assetId: assetId,
|
||||
hash: Uint8List.fromList(hash),
|
||||
modifiedTime: modifiedTime,
|
||||
);
|
||||
|
||||
static DeviceAssetEntity fromDto(DeviceAsset dto) => DeviceAssetEntity(
|
||||
assetId: dto.assetId,
|
||||
hash: dto.hash,
|
||||
modifiedTime: dto.modifiedTime,
|
||||
);
|
||||
}
|
||||
895
mobile/lib/infrastructure/entities/device_asset.entity.g.dart
generated
Normal file
895
mobile/lib/infrastructure/entities/device_asset.entity.g.dart
generated
Normal file
@@ -0,0 +1,895 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'device_asset.entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetDeviceAssetEntityCollection on Isar {
|
||||
IsarCollection<DeviceAssetEntity> get deviceAssetEntitys => this.collection();
|
||||
}
|
||||
|
||||
const DeviceAssetEntitySchema = CollectionSchema(
|
||||
name: r'DeviceAssetEntity',
|
||||
id: 6967030785073446271,
|
||||
properties: {
|
||||
r'assetId': PropertySchema(
|
||||
id: 0,
|
||||
name: r'assetId',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'hash': PropertySchema(
|
||||
id: 1,
|
||||
name: r'hash',
|
||||
type: IsarType.byteList,
|
||||
),
|
||||
r'modifiedTime': PropertySchema(
|
||||
id: 2,
|
||||
name: r'modifiedTime',
|
||||
type: IsarType.dateTime,
|
||||
)
|
||||
},
|
||||
estimateSize: _deviceAssetEntityEstimateSize,
|
||||
serialize: _deviceAssetEntitySerialize,
|
||||
deserialize: _deviceAssetEntityDeserialize,
|
||||
deserializeProp: _deviceAssetEntityDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {
|
||||
r'assetId': IndexSchema(
|
||||
id: 174362542210192109,
|
||||
name: r'assetId',
|
||||
unique: true,
|
||||
replace: true,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'assetId',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: true,
|
||||
)
|
||||
],
|
||||
),
|
||||
r'hash': IndexSchema(
|
||||
id: -7973251393006690288,
|
||||
name: r'hash',
|
||||
unique: false,
|
||||
replace: false,
|
||||
properties: [
|
||||
IndexPropertySchema(
|
||||
name: r'hash',
|
||||
type: IndexType.hash,
|
||||
caseSensitive: false,
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _deviceAssetEntityGetId,
|
||||
getLinks: _deviceAssetEntityGetLinks,
|
||||
attach: _deviceAssetEntityAttach,
|
||||
version: '3.1.8',
|
||||
);
|
||||
|
||||
int _deviceAssetEntityEstimateSize(
|
||||
DeviceAssetEntity object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
bytesCount += 3 + object.assetId.length * 3;
|
||||
bytesCount += 3 + object.hash.length;
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _deviceAssetEntitySerialize(
|
||||
DeviceAssetEntity object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeString(offsets[0], object.assetId);
|
||||
writer.writeByteList(offsets[1], object.hash);
|
||||
writer.writeDateTime(offsets[2], object.modifiedTime);
|
||||
}
|
||||
|
||||
DeviceAssetEntity _deviceAssetEntityDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = DeviceAssetEntity(
|
||||
assetId: reader.readString(offsets[0]),
|
||||
hash: reader.readByteList(offsets[1]) ?? [],
|
||||
modifiedTime: reader.readDateTime(offsets[2]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _deviceAssetEntityDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 1:
|
||||
return (reader.readByteList(offset) ?? []) as P;
|
||||
case 2:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _deviceAssetEntityGetId(DeviceAssetEntity object) {
|
||||
return object.id;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _deviceAssetEntityGetLinks(
|
||||
DeviceAssetEntity object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _deviceAssetEntityAttach(
|
||||
IsarCollection<dynamic> col, Id id, DeviceAssetEntity object) {}
|
||||
|
||||
extension DeviceAssetEntityByIndex on IsarCollection<DeviceAssetEntity> {
|
||||
Future<DeviceAssetEntity?> getByAssetId(String assetId) {
|
||||
return getByIndex(r'assetId', [assetId]);
|
||||
}
|
||||
|
||||
DeviceAssetEntity? getByAssetIdSync(String assetId) {
|
||||
return getByIndexSync(r'assetId', [assetId]);
|
||||
}
|
||||
|
||||
Future<bool> deleteByAssetId(String assetId) {
|
||||
return deleteByIndex(r'assetId', [assetId]);
|
||||
}
|
||||
|
||||
bool deleteByAssetIdSync(String assetId) {
|
||||
return deleteByIndexSync(r'assetId', [assetId]);
|
||||
}
|
||||
|
||||
Future<List<DeviceAssetEntity?>> getAllByAssetId(List<String> assetIdValues) {
|
||||
final values = assetIdValues.map((e) => [e]).toList();
|
||||
return getAllByIndex(r'assetId', values);
|
||||
}
|
||||
|
||||
List<DeviceAssetEntity?> getAllByAssetIdSync(List<String> assetIdValues) {
|
||||
final values = assetIdValues.map((e) => [e]).toList();
|
||||
return getAllByIndexSync(r'assetId', values);
|
||||
}
|
||||
|
||||
Future<int> deleteAllByAssetId(List<String> assetIdValues) {
|
||||
final values = assetIdValues.map((e) => [e]).toList();
|
||||
return deleteAllByIndex(r'assetId', values);
|
||||
}
|
||||
|
||||
int deleteAllByAssetIdSync(List<String> assetIdValues) {
|
||||
final values = assetIdValues.map((e) => [e]).toList();
|
||||
return deleteAllByIndexSync(r'assetId', values);
|
||||
}
|
||||
|
||||
Future<Id> putByAssetId(DeviceAssetEntity object) {
|
||||
return putByIndex(r'assetId', object);
|
||||
}
|
||||
|
||||
Id putByAssetIdSync(DeviceAssetEntity object, {bool saveLinks = true}) {
|
||||
return putByIndexSync(r'assetId', object, saveLinks: saveLinks);
|
||||
}
|
||||
|
||||
Future<List<Id>> putAllByAssetId(List<DeviceAssetEntity> objects) {
|
||||
return putAllByIndex(r'assetId', objects);
|
||||
}
|
||||
|
||||
List<Id> putAllByAssetIdSync(List<DeviceAssetEntity> objects,
|
||||
{bool saveLinks = true}) {
|
||||
return putAllByIndexSync(r'assetId', objects, saveLinks: saveLinks);
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQueryWhereSort
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QWhere> {
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQueryWhere
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QWhereClause> {
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
idEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: id,
|
||||
upper: id,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
idNotEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
idGreaterThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
idLessThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
idBetween(
|
||||
Id lowerId,
|
||||
Id upperId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerId,
|
||||
includeLower: includeLower,
|
||||
upper: upperId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
assetIdEqualTo(String assetId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'assetId',
|
||||
value: [assetId],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
assetIdNotEqualTo(String assetId) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'assetId',
|
||||
lower: [],
|
||||
upper: [assetId],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'assetId',
|
||||
lower: [assetId],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'assetId',
|
||||
lower: [assetId],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'assetId',
|
||||
lower: [],
|
||||
upper: [assetId],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
hashEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IndexWhereClause.equalTo(
|
||||
indexName: r'hash',
|
||||
value: [hash],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterWhereClause>
|
||||
hashNotEqualTo(List<int> hash) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
));
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [hash],
|
||||
includeLower: false,
|
||||
upper: [],
|
||||
))
|
||||
.addWhereClause(IndexWhereClause.between(
|
||||
indexName: r'hash',
|
||||
lower: [],
|
||||
upper: [hash],
|
||||
includeUpper: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQueryFilter
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QFilterCondition> {
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'assetId',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'assetId',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'assetId',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'assetId',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'assetId',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'assetId',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'assetId',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'assetId',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'assetId',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
assetIdIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'assetId',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashElementEqualTo(int value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashElementGreaterThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashElementLessThan(
|
||||
int value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'hash',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashElementBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'hash',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
hashLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'hash',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
idEqualTo(Id value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
Id value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
idBetween(
|
||||
Id lower,
|
||||
Id upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
modifiedTimeEqualTo(DateTime value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'modifiedTime',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
modifiedTimeGreaterThan(
|
||||
DateTime value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'modifiedTime',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
modifiedTimeLessThan(
|
||||
DateTime value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'modifiedTime',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterFilterCondition>
|
||||
modifiedTimeBetween(
|
||||
DateTime lower,
|
||||
DateTime upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'modifiedTime',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQueryObject
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QFilterCondition> {}
|
||||
|
||||
extension DeviceAssetEntityQueryLinks
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QFilterCondition> {}
|
||||
|
||||
extension DeviceAssetEntityQuerySortBy
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QSortBy> {
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
sortByAssetId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'assetId', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
sortByAssetIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'assetId', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
sortByModifiedTime() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'modifiedTime', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
sortByModifiedTimeDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'modifiedTime', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQuerySortThenBy
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QSortThenBy> {
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
thenByAssetId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'assetId', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
thenByAssetIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'assetId', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
thenByModifiedTime() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'modifiedTime', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QAfterSortBy>
|
||||
thenByModifiedTimeDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'modifiedTime', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQueryWhereDistinct
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QDistinct> {
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QDistinct>
|
||||
distinctByAssetId({bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'assetId', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QDistinct>
|
||||
distinctByHash() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QDistinct>
|
||||
distinctByModifiedTime() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'modifiedTime');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceAssetEntityQueryProperty
|
||||
on QueryBuilder<DeviceAssetEntity, DeviceAssetEntity, QQueryProperty> {
|
||||
QueryBuilder<DeviceAssetEntity, int, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, String, QQueryOperations> assetIdProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'assetId');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, List<int>, QQueryOperations> hashProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'hash');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<DeviceAssetEntity, DateTime, QQueryOperations>
|
||||
modifiedTimeProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'modifiedTime');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class IsarDeviceAssetRepository extends IsarDatabaseRepository
|
||||
implements IDeviceAssetRepository {
|
||||
final Isar _db;
|
||||
|
||||
const IsarDeviceAssetRepository(this._db) : super(_db);
|
||||
|
||||
@override
|
||||
Future<void> deleteIds(List<String> ids) {
|
||||
return transaction(() async {
|
||||
await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceAsset>> getByIds(List<String> localIds) {
|
||||
return _db.deviceAssetEntitys
|
||||
.where()
|
||||
.anyOf(localIds, (query, id) => query.assetIdEqualTo(id))
|
||||
.findAll()
|
||||
.then((value) => value.map((e) => e.toModel()).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateAll(List<DeviceAsset> assetHash) {
|
||||
return transaction(() async {
|
||||
await _db.deviceAssetEntitys
|
||||
.putAll(assetHash.map(DeviceAssetEntity.fromDto).toList());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import 'package:isar/isar.dart';
|
||||
class IsarStoreRepository extends IsarDatabaseRepository
|
||||
implements IStoreRepository {
|
||||
final Isar _db;
|
||||
const IsarStoreRepository(super.db) : _db = db;
|
||||
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
||||
|
||||
IsarStoreRepository(super.db) : _db = db;
|
||||
|
||||
@override
|
||||
Future<bool> deleteAll() async {
|
||||
@@ -21,9 +23,14 @@ class IsarStoreRepository extends IsarDatabaseRepository
|
||||
|
||||
@override
|
||||
Stream<StoreUpdateEvent> watchAll() {
|
||||
return _db.storeValues.where().watch(fireImmediately: true).asyncExpand(
|
||||
(entities) =>
|
||||
Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e))),
|
||||
return _db.storeValues
|
||||
.filter()
|
||||
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
|
||||
.watch(fireImmediately: true)
|
||||
.asyncExpand(
|
||||
(entities) => Stream.fromFutures(
|
||||
entities.map((e) async => _toUpdateEvent(e)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IAssetRepository implements IDatabaseRepository {
|
||||
@@ -50,10 +49,6 @@ abstract interface class IAssetRepository implements IDatabaseRepository {
|
||||
int limit = 100,
|
||||
});
|
||||
|
||||
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
|
||||
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
|
||||
|
||||
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
|
||||
|
||||
Future<List<String>> getAllDuplicatedAssetIds();
|
||||
|
||||
5
mobile/lib/interfaces/local_files_manager.interface.dart
Normal file
5
mobile/lib/interfaces/local_files_manager.interface.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
abstract interface class ILocalFilesManager {
|
||||
Future<bool> moveToTrash(String fileName);
|
||||
Future<bool> restoreFromTrash(String fileName);
|
||||
Future<bool> requestManageStoragePermission();
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/download_panel.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/gallery_stacked_children.dart';
|
||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
@@ -63,6 +63,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final loadAsset = renderList.loadAsset;
|
||||
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
||||
|
||||
// This key is to prevent the video player from being re-initialized during
|
||||
// hero animation or device rotation.
|
||||
final videoPlayerKey = useMemoized(() => GlobalKey());
|
||||
|
||||
Future<void> precacheNextImage(int index) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
@@ -225,8 +229,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) {
|
||||
// This key is to prevent the video player from being re-initialized during the hero animation
|
||||
final key = GlobalKey();
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
onDragStart: (_, details, __) =>
|
||||
localPosition.value = details.localPosition,
|
||||
@@ -241,7 +243,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
width: context.width,
|
||||
height: context.height,
|
||||
child: NativeVideoViewerPage(
|
||||
key: key,
|
||||
key: videoPlayerKey,
|
||||
asset: asset,
|
||||
image: Image(
|
||||
key: ValueKey(asset),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final deviceAssetRepositoryProvider = Provider<IDeviceAssetRepository>(
|
||||
(ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)),
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
@@ -36,10 +37,13 @@ final currentUserProvider =
|
||||
|
||||
class TimelineUserIdsProvider extends StateNotifier<List<String>> {
|
||||
TimelineUserIdsProvider(this._timelineService) : super([]) {
|
||||
final listEquality = const ListEquality();
|
||||
_timelineService.getTimelineUserIds().then((users) => state = users);
|
||||
streamSub = _timelineService
|
||||
.watchTimelineUserIds()
|
||||
.listen((users) => state = users);
|
||||
streamSub = _timelineService.watchTimelineUserIds().listen((users) {
|
||||
if (!listEquality.equals(state, users)) {
|
||||
state = users;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
late final StreamSubscription<List<String>> streamSub;
|
||||
|
||||
@@ -23,6 +23,7 @@ enum PendingAction {
|
||||
assetDelete,
|
||||
assetUploaded,
|
||||
assetHidden,
|
||||
assetTrash,
|
||||
}
|
||||
|
||||
class PendingChange {
|
||||
@@ -160,7 +161,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||
socket.on('on_asset_trash', _handleServerUpdates);
|
||||
socket.on('on_asset_trash', _handleOnAssetTrash);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
socket.on('on_asset_update', _handleServerUpdates);
|
||||
socket.on('on_asset_stack_update', _handleServerUpdates);
|
||||
@@ -207,6 +208,26 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
_debounce.run(handlePendingChanges);
|
||||
}
|
||||
|
||||
Future<void> _handlePendingTrashes() async {
|
||||
final trashChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetTrash)
|
||||
.toList();
|
||||
if (trashChanges.isNotEmpty) {
|
||||
List<String> remoteIds = trashChanges
|
||||
.expand((a) => (a.value as List).map((e) => e.toString()))
|
||||
.toList();
|
||||
|
||||
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.whereNot((c) => trashChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePendingDeletes() async {
|
||||
final deleteChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetDelete)
|
||||
@@ -267,6 +288,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
await _handlePendingUploaded();
|
||||
await _handlePendingDeletes();
|
||||
await _handlingPendingHidden();
|
||||
await _handlePendingTrashes();
|
||||
}
|
||||
|
||||
void _handleOnConfigUpdate(dynamic _) {
|
||||
@@ -285,6 +307,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
void _handleOnAssetDelete(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetDelete, data);
|
||||
|
||||
void _handleOnAssetTrash(dynamic data) {
|
||||
addPendingChange(PendingAction.assetTrash, data);
|
||||
}
|
||||
|
||||
void _handleOnAssetHidden(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetHidden, data);
|
||||
|
||||
|
||||
@@ -8,16 +8,23 @@ import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||
|
||||
final albumMediaRepositoryProvider = Provider((ref) => AlbumMediaRepository());
|
||||
final albumMediaRepositoryProvider =
|
||||
Provider((ref) => const AlbumMediaRepository());
|
||||
|
||||
class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
const AlbumMediaRepository();
|
||||
|
||||
bool get useCustomFilter =>
|
||||
Store.get(StoreKey.photoManagerCustomFilter, false);
|
||||
|
||||
@override
|
||||
Future<List<Album>> getAll() async {
|
||||
final filter = useCustomFilter
|
||||
? CustomFilter.sql(where: '${CustomColumns.base.width} > 0')
|
||||
: FilterOptionGroup(containsPathModified: true);
|
||||
|
||||
final List<AssetPathEntity> assetPathEntities =
|
||||
await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
filterOption: FilterOptionGroup(containsPathModified: true),
|
||||
);
|
||||
await PhotoManager.getAssetPathList(hasAll: true, filterOption: filter);
|
||||
return assetPathEntities.map(_toAlbum).toList();
|
||||
}
|
||||
|
||||
@@ -47,18 +54,18 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
final onDevice = await AssetPathEntity.fromId(
|
||||
albumId,
|
||||
filterOption: FilterOptionGroup(
|
||||
containsPathModified: true,
|
||||
orders: orderByModificationDate
|
||||
? [const OrderOption(type: OrderOptionType.updateDate)]
|
||||
: [],
|
||||
imageOption: const FilterOption(needTitle: true),
|
||||
videoOption: const FilterOption(needTitle: true),
|
||||
containsPathModified: true,
|
||||
updateTimeCond: modifiedFrom == null && modifiedUntil == null
|
||||
? null
|
||||
: DateTimeCond(
|
||||
min: modifiedFrom ?? DateTime.utc(-271820),
|
||||
max: modifiedUntil ?? DateTime.utc(275760),
|
||||
),
|
||||
orders: orderByModificationDate
|
||||
? [const OrderOption(type: OrderOptionType.updateDate)]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
@@ -158,19 +153,6 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
return _getMatchesImpl(query, fastHash(ownerId), assets, limit);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
|
||||
Platform.isAndroid
|
||||
? db.androidDeviceAssets.getAll(ids.cast())
|
||||
: db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
|
||||
@override
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) => txn(
|
||||
() => Platform.isAndroid
|
||||
? db.androidDeviceAssets.putAll(deviceAssets.cast())
|
||||
: db.iOSDeviceAssets.putAll(deviceAssets.cast()),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Asset> update(Asset asset) async {
|
||||
await txn(() => asset.put(db));
|
||||
|
||||
23
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
23
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/utils/local_files_manager.dart';
|
||||
|
||||
final localFilesManagerRepositoryProvider =
|
||||
Provider((ref) => LocalFilesManagerRepository());
|
||||
|
||||
class LocalFilesManagerRepository implements ILocalFilesManager {
|
||||
@override
|
||||
Future<bool> moveToTrash(String fileName) async {
|
||||
return await LocalFilesManager.moveToTrash(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> restoreFromTrash(String fileName) async {
|
||||
return await LocalFilesManager.restoreFromTrash(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestManageStoragePermission() async {
|
||||
return await LocalFilesManager.requestManageStoragePermission();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ enum AppSettingsEnum<T> {
|
||||
0,
|
||||
),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
@@ -84,6 +85,11 @@ enum AppSettingsEnum<T> {
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
|
||||
photoManagerCustomFilter<bool>(
|
||||
StoreKey.photoManagerCustomFilter,
|
||||
null,
|
||||
false,
|
||||
),
|
||||
;
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
@@ -256,7 +256,7 @@ class AssetService {
|
||||
|
||||
for (var element in assets) {
|
||||
element.fileCreatedAt = DateTime.parse(updatedDt);
|
||||
element.exifInfo ??= element.exifInfo
|
||||
element.exifInfo = element.exifInfo
|
||||
?.copyWith(dateTimeOriginal: DateTime.parse(updatedDt));
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ class AssetService {
|
||||
);
|
||||
|
||||
for (var element in assets) {
|
||||
element.exifInfo ??= element.exifInfo?.copyWith(
|
||||
element.exifInfo = element.exifInfo?.copyWith(
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
);
|
||||
|
||||
@@ -1,172 +1,205 @@
|
||||
// ignore_for_file: avoid-unsafe-collection-methods
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/device_asset.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/device_asset.provider.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class HashService {
|
||||
HashService(
|
||||
this._assetRepository,
|
||||
this._backgroundService,
|
||||
this._albumMediaRepository,
|
||||
);
|
||||
final IAssetRepository _assetRepository;
|
||||
final BackgroundService _backgroundService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final _log = Logger('HashService');
|
||||
HashService({
|
||||
required IDeviceAssetRepository deviceAssetRepository,
|
||||
required BackgroundService backgroundService,
|
||||
this.batchSizeLimit = kBatchHashSizeLimit,
|
||||
this.batchFileLimit = kBatchHashFileLimit,
|
||||
}) : _deviceAssetRepository = deviceAssetRepository,
|
||||
_backgroundService = backgroundService;
|
||||
|
||||
/// Returns all assets that were successfully hashed
|
||||
Future<List<Asset>> getHashedAssets(
|
||||
Album album, {
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
DateTime? modifiedFrom,
|
||||
DateTime? modifiedUntil,
|
||||
Set<String>? excludedAssets,
|
||||
}) async {
|
||||
final entities = await _albumMediaRepository.getAssets(
|
||||
album.localId!,
|
||||
start: start,
|
||||
end: end,
|
||||
modifiedFrom: modifiedFrom,
|
||||
modifiedUntil: modifiedUntil,
|
||||
);
|
||||
final filtered = excludedAssets == null
|
||||
? entities
|
||||
: entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
|
||||
return _hashAssets(filtered);
|
||||
}
|
||||
final IDeviceAssetRepository _deviceAssetRepository;
|
||||
final BackgroundService _backgroundService;
|
||||
final int batchSizeLimit;
|
||||
final int batchFileLimit;
|
||||
final _log = Logger('HashService');
|
||||
|
||||
/// Processes a list of local [Asset]s, storing their hash and returning only those
|
||||
/// that were successfully hashed. Hashes are looked up in a DB table
|
||||
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
|
||||
/// entries are newly hashed and added to the DB table.
|
||||
Future<List<Asset>> _hashAssets(List<Asset> assets) async {
|
||||
const int batchFileCount = 128;
|
||||
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
|
||||
/// [DeviceAsset] by local id. Only missing entries are newly hashed and added to the DB table.
|
||||
Future<List<Asset>> hashAssets(List<Asset> assets) async {
|
||||
assets.sort(Asset.compareByLocalId);
|
||||
|
||||
final ids = assets
|
||||
.map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
|
||||
.toList();
|
||||
final List<DeviceAsset?> hashes =
|
||||
await _assetRepository.getDeviceAssetsById(ids);
|
||||
final List<DeviceAsset> toAdd = [];
|
||||
final List<String> toHash = [];
|
||||
// Get and sort DB entries - guaranteed to be a subset of assets
|
||||
final hashesInDB = await _deviceAssetRepository.getByIds(
|
||||
assets.map((a) => a.localId!).toList(),
|
||||
);
|
||||
hashesInDB.sort((a, b) => a.assetId.compareTo(b.assetId));
|
||||
|
||||
int bytes = 0;
|
||||
int dbIndex = 0;
|
||||
int bytesProcessed = 0;
|
||||
final hashedAssets = <Asset>[];
|
||||
final toBeHashed = <_AssetPath>[];
|
||||
final toBeDeleted = <String>[];
|
||||
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
if (hashes[i] != null) {
|
||||
for (int assetIndex = 0; assetIndex < assets.length; assetIndex++) {
|
||||
final asset = assets[assetIndex];
|
||||
DeviceAsset? matchingDbEntry;
|
||||
|
||||
if (dbIndex < hashesInDB.length) {
|
||||
final deviceAsset = hashesInDB[dbIndex];
|
||||
if (deviceAsset.assetId == asset.localId) {
|
||||
matchingDbEntry = deviceAsset;
|
||||
dbIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingDbEntry != null &&
|
||||
matchingDbEntry.hash.isNotEmpty &&
|
||||
matchingDbEntry.modifiedTime.isAtSameMomentAs(asset.fileModifiedAt)) {
|
||||
// Reuse the existing hash
|
||||
hashedAssets.add(
|
||||
asset.copyWith(checksum: base64.encode(matchingDbEntry.hash)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
File? file;
|
||||
|
||||
try {
|
||||
file = await assets[i].local!.originFile;
|
||||
} catch (error, stackTrace) {
|
||||
_log.warning(
|
||||
"Error getting file to hash for asset ${assets[i].localId}, name: ${assets[i].fileName}, created on: ${assets[i].fileCreatedAt}, skipping",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
final file = await _tryGetAssetFile(asset);
|
||||
if (file == null) {
|
||||
final fileName = assets[i].fileName;
|
||||
|
||||
_log.warning(
|
||||
"Failed to get file for asset ${assets[i].localId}, name: $fileName, created on: ${assets[i].fileCreatedAt}, skipping",
|
||||
);
|
||||
// Can't access file, delete any DB entry
|
||||
if (matchingDbEntry != null) {
|
||||
toBeDeleted.add(matchingDbEntry.assetId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
bytes += await file.length();
|
||||
toHash.add(file.path);
|
||||
final deviceAsset = Platform.isAndroid
|
||||
? AndroidDeviceAsset(id: ids[i] as int, hash: const [])
|
||||
: IOSDeviceAsset(id: ids[i] as String, hash: const []);
|
||||
toAdd.add(deviceAsset);
|
||||
hashes[i] = deviceAsset;
|
||||
if (toHash.length == batchFileCount || bytes >= batchDataSize) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
toAdd.clear();
|
||||
toHash.clear();
|
||||
bytes = 0;
|
||||
|
||||
bytesProcessed += await file.length();
|
||||
toBeHashed.add(_AssetPath(asset: asset, path: file.path));
|
||||
|
||||
if (_shouldProcessBatch(toBeHashed.length, bytesProcessed)) {
|
||||
hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted));
|
||||
toBeHashed.clear();
|
||||
toBeDeleted.clear();
|
||||
bytesProcessed = 0;
|
||||
}
|
||||
}
|
||||
if (toHash.isNotEmpty) {
|
||||
await _processBatch(toHash, toAdd);
|
||||
assert(dbIndex == hashesInDB.length, "All hashes should've been processed");
|
||||
|
||||
// Process any remaining files
|
||||
if (toBeHashed.isNotEmpty) {
|
||||
hashedAssets.addAll(await _processBatch(toBeHashed, toBeDeleted));
|
||||
}
|
||||
return _getHashedAssets(assets, hashes);
|
||||
|
||||
// Clean up deleted references
|
||||
if (toBeDeleted.isNotEmpty) {
|
||||
await _deviceAssetRepository.deleteIds(toBeDeleted);
|
||||
}
|
||||
|
||||
return hashedAssets;
|
||||
}
|
||||
|
||||
/// Processes a batch of files and saves any successfully hashed
|
||||
/// values to the DB table.
|
||||
Future<void> _processBatch(
|
||||
final List<String> toHash,
|
||||
final List<DeviceAsset> toAdd,
|
||||
bool _shouldProcessBatch(int assetCount, int bytesProcessed) =>
|
||||
assetCount >= batchFileLimit || bytesProcessed >= batchSizeLimit;
|
||||
|
||||
Future<File?> _tryGetAssetFile(Asset asset) async {
|
||||
try {
|
||||
final file = await asset.local!.originFile;
|
||||
if (file == null) {
|
||||
_log.warning(
|
||||
"Failed to get file for asset ${asset.localId ?? '<N/A>'}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return file;
|
||||
} catch (error, stackTrace) {
|
||||
_log.warning(
|
||||
"Error getting file to hash for asset ${asset.localId ?? '<N/A>'}, name: ${asset.fileName}, created on: ${asset.fileCreatedAt}, skipping",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a batch of files and returns a list of successfully hashed assets after saving
|
||||
/// them in [DeviceAssetToHash] for future retrieval
|
||||
Future<List<Asset>> _processBatch(
|
||||
List<_AssetPath> toBeHashed,
|
||||
List<String> toBeDeleted,
|
||||
) async {
|
||||
final hashes = await _hashFiles(toHash);
|
||||
bool anyNull = false;
|
||||
for (int j = 0; j < hashes.length; j++) {
|
||||
if (hashes[j]?.length == 20) {
|
||||
toAdd[j].hash = hashes[j]!;
|
||||
_log.info("Hashing ${toBeHashed.length} files");
|
||||
final hashes = await _hashFiles(toBeHashed.map((e) => e.path).toList());
|
||||
assert(
|
||||
hashes.length == toBeHashed.length,
|
||||
"Number of Hashes returned from platform should be the same as the input",
|
||||
);
|
||||
|
||||
final hashedAssets = <Asset>[];
|
||||
final toBeAdded = <DeviceAsset>[];
|
||||
|
||||
for (final (index, hash) in hashes.indexed) {
|
||||
final asset = toBeHashed.elementAtOrNull(index)?.asset;
|
||||
if (asset != null && hash?.length == 20) {
|
||||
hashedAssets.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||
toBeAdded.add(
|
||||
DeviceAsset(
|
||||
assetId: asset.localId!,
|
||||
hash: hash,
|
||||
modifiedTime: asset.fileModifiedAt,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_log.warning("Failed to hash file ${toHash[j]}, skipping");
|
||||
anyNull = true;
|
||||
_log.warning("Failed to hash file ${asset?.localId ?? '<null>'}");
|
||||
if (asset != null) {
|
||||
toBeDeleted.add(asset.localId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
|
||||
await _assetRepository
|
||||
.transaction(() => _assetRepository.upsertDeviceAssets(validHashes));
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
// Update the DB for future retrieval
|
||||
await _deviceAssetRepository.transaction(() async {
|
||||
await _deviceAssetRepository.updateAll(toBeAdded);
|
||||
await _deviceAssetRepository.deleteIds(toBeDeleted);
|
||||
});
|
||||
|
||||
_log.fine("Hashed ${hashedAssets.length}/${toBeHashed.length} assets");
|
||||
return hashedAssets;
|
||||
}
|
||||
|
||||
/// Hashes the given files and returns a list of the same length
|
||||
/// files that could not be hashed have a `null` value
|
||||
/// Hashes the given files and returns a list of the same length.
|
||||
/// Files that could not be hashed will have a `null` value
|
||||
Future<List<Uint8List?>> _hashFiles(List<String> paths) async {
|
||||
final List<Uint8List?>? hashes =
|
||||
await _backgroundService.digestFiles(paths);
|
||||
if (hashes == null) {
|
||||
throw Exception("Hashing ${paths.length} files failed");
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
/// Returns all successfully hashed [Asset]s with their hash value set
|
||||
List<Asset> _getHashedAssets(
|
||||
List<Asset> assets,
|
||||
List<DeviceAsset?> hashes,
|
||||
) {
|
||||
final List<Asset> result = [];
|
||||
for (int i = 0; i < assets.length; i++) {
|
||||
if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) {
|
||||
assets[i].byteHash = hashes[i]!.hash;
|
||||
result.add(assets[i]);
|
||||
try {
|
||||
final hashes = await _backgroundService.digestFiles(paths);
|
||||
if (hashes != null) {
|
||||
return hashes;
|
||||
}
|
||||
_log.severe("Hashing ${paths.length} files failed");
|
||||
} catch (e, s) {
|
||||
_log.severe("Error occurred while hashing assets", e, s);
|
||||
}
|
||||
return result;
|
||||
return List.filled(paths.length, null);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetPath {
|
||||
final Asset asset;
|
||||
final String path;
|
||||
|
||||
const _AssetPath({required this.asset, required this.path});
|
||||
|
||||
_AssetPath copyWith({Asset? asset, String? path}) {
|
||||
return _AssetPath(asset: asset ?? this.asset, path: path ?? this.path);
|
||||
}
|
||||
}
|
||||
|
||||
final hashServiceProvider = Provider(
|
||||
(ref) => HashService(
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
deviceAssetRepository: ref.watch(deviceAssetRepositoryProvider),
|
||||
backgroundService: ref.watch(backgroundServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -16,6 +17,8 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||
@@ -25,6 +28,8 @@ import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/repositories/partner.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
@@ -48,6 +53,8 @@ final syncServiceProvider = Provider(
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(localFilesManagerRepositoryProvider),
|
||||
ref.watch(partnerApiRepositoryProvider),
|
||||
ref.watch(userApiRepositoryProvider),
|
||||
),
|
||||
@@ -69,6 +76,8 @@ class SyncService {
|
||||
final IUserApiRepository _userApiRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
final AppSettingsService _appSettingsService;
|
||||
final ILocalFilesManager _localFilesManager;
|
||||
|
||||
SyncService(
|
||||
this._hashService,
|
||||
@@ -82,6 +91,8 @@ class SyncService {
|
||||
this._userRepository,
|
||||
this._userService,
|
||||
this._eTagRepository,
|
||||
this._appSettingsService,
|
||||
this._localFilesManager,
|
||||
this._partnerApiRepository,
|
||||
this._userApiRepository,
|
||||
);
|
||||
@@ -238,8 +249,19 @@ class SyncService {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _moveToTrashMatchedAssets(Iterable<String> idsToDelete) async {
|
||||
final List<Asset> localAssets = await _assetRepository.getAllLocal();
|
||||
final List<Asset> matchedAssets = localAssets
|
||||
.where((asset) => idsToDelete.contains(asset.remoteId))
|
||||
.toList();
|
||||
|
||||
for (var asset in matchedAssets) {
|
||||
_localFilesManager.moveToTrash(asset.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) async {
|
||||
return _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
idsToDelete,
|
||||
@@ -249,6 +271,12 @@ class SyncService {
|
||||
idsToDelete,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
await _moveToTrashMatchedAssets(idsToDelete);
|
||||
}
|
||||
if (merged.isEmpty) return;
|
||||
for (final Asset asset in merged) {
|
||||
asset.remoteId = null;
|
||||
@@ -577,15 +605,18 @@ class SyncService {
|
||||
Set<String>? excludedAssets,
|
||||
bool forceRefresh = false,
|
||||
]) async {
|
||||
_log.info("Syncing a local album to DB: ${deviceAlbum.name}");
|
||||
if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) {
|
||||
_log.fine(
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} has not changed. Skipping sync.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
_log.info("Local album ${deviceAlbum.name} has changed. Syncing...");
|
||||
if (!forceRefresh &&
|
||||
excludedAssets == null &&
|
||||
await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
|
||||
_log.info("Fast synced local album ${deviceAlbum.name} to DB");
|
||||
return true;
|
||||
}
|
||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||
@@ -598,7 +629,7 @@ class SyncService {
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final List<Asset> onDevice = await _hashService.getHashedAssets(
|
||||
final List<Asset> onDevice = await _getHashedAssets(
|
||||
deviceAlbum,
|
||||
excludedAssets: excludedAssets,
|
||||
);
|
||||
@@ -611,7 +642,7 @@ class SyncService {
|
||||
dbAlbum.name == deviceAlbum.name &&
|
||||
dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) {
|
||||
// changes only affeted excluded albums
|
||||
_log.fine(
|
||||
_log.info(
|
||||
"Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
|
||||
);
|
||||
if (assetCountOnDevice !=
|
||||
@@ -626,11 +657,11 @@ class SyncService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_log.fine(
|
||||
_log.info(
|
||||
"Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
|
||||
);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
||||
_log.fine(
|
||||
_log.info(
|
||||
"Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update",
|
||||
);
|
||||
deleteCandidates.addAll(toDelete);
|
||||
@@ -667,6 +698,9 @@ class SyncService {
|
||||
/// returns `true` if successful, else `false`
|
||||
Future<bool> _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async {
|
||||
if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) {
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} has not changed. Skipping sync.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final int totalOnDevice =
|
||||
@@ -676,15 +710,21 @@ class SyncService {
|
||||
?.assetCount ??
|
||||
0;
|
||||
if (totalOnDevice <= lastKnownTotal) {
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} totalOnDevice is less than lastKnownTotal. Skipping sync.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final List<Asset> newAssets = await _hashService.getHashedAssets(
|
||||
final List<Asset> newAssets = await _getHashedAssets(
|
||||
deviceAlbum,
|
||||
modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)),
|
||||
modifiedUntil: deviceAlbum.modifiedAt,
|
||||
);
|
||||
|
||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||
_log.info(
|
||||
"Local album ${deviceAlbum.name} totalOnDevice is not equal to lastKnownTotal + newAssets.length. Skipping sync.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
dbAlbum.modifiedAt = deviceAlbum.modifiedAt;
|
||||
@@ -719,8 +759,8 @@ class SyncService {
|
||||
List<Asset> existing, [
|
||||
Set<String>? excludedAssets,
|
||||
]) async {
|
||||
_log.info("Syncing a new local album to DB: ${album.name}");
|
||||
final assets = await _hashService.getHashedAssets(
|
||||
_log.info("Adding a new local album to DB: ${album.name}");
|
||||
final assets = await _getHashedAssets(
|
||||
album,
|
||||
excludedAssets: excludedAssets,
|
||||
);
|
||||
@@ -778,15 +818,33 @@ class SyncService {
|
||||
return (existing, toUpsert);
|
||||
}
|
||||
|
||||
Future<void> _toggleTrashStatusForAssets(List<Asset> assetsList) async {
|
||||
for (var asset in assetsList) {
|
||||
if (asset.isTrashed) {
|
||||
_localFilesManager.moveToTrash(asset.fileName);
|
||||
} else {
|
||||
_localFilesManager.restoreFromTrash(asset.fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||
if (assets.isEmpty) return;
|
||||
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
_toggleTrashStatusForAssets(assets);
|
||||
}
|
||||
|
||||
final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList();
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(assets);
|
||||
for (final Asset added in assets) {
|
||||
added.exifInfo ??= added.exifInfo?.copyWith(assetId: added.id);
|
||||
added.exifInfo = added.exifInfo?.copyWith(assetId: added.id);
|
||||
}
|
||||
await _exifInfoRepository.updateAll(exifInfos);
|
||||
});
|
||||
@@ -824,6 +882,28 @@ class SyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all assets that were successfully hashed
|
||||
Future<List<Asset>> _getHashedAssets(
|
||||
Album album, {
|
||||
int start = 0,
|
||||
int end = 0x7fffffffffffffff,
|
||||
DateTime? modifiedFrom,
|
||||
DateTime? modifiedUntil,
|
||||
Set<String>? excludedAssets,
|
||||
}) async {
|
||||
final entities = await _albumMediaRepository.getAssets(
|
||||
album.localId!,
|
||||
start: start,
|
||||
end: end,
|
||||
modifiedFrom: modifiedFrom,
|
||||
modifiedUntil: modifiedUntil,
|
||||
);
|
||||
final filtered = excludedAssets == null
|
||||
? entities
|
||||
: entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
|
||||
return _hashService.hashAssets(filtered);
|
||||
}
|
||||
|
||||
List<Asset> _removeDuplicates(List<Asset> assets) {
|
||||
final int before = assets.length;
|
||||
assets.sort(Asset.compareByOwnerChecksumCreatedModified);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
@@ -39,9 +40,10 @@ abstract final class Bootstrap {
|
||||
ETagSchema,
|
||||
if (Platform.isAndroid) AndroidDeviceAssetSchema,
|
||||
if (Platform.isIOS) IOSDeviceAssetSchema,
|
||||
DeviceAssetEntitySchema,
|
||||
],
|
||||
directory: dir.path,
|
||||
maxSizeMiB: 1024,
|
||||
maxSizeMiB: 2048,
|
||||
inspector: kDebugMode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,3 +75,17 @@ bool diffSortedListsSync<T>(
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
int compareToNullable<T extends Comparable>(T? a, T? b) {
|
||||
if (a == null && b == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a == null) {
|
||||
return 1;
|
||||
}
|
||||
if (b == null) {
|
||||
return -1;
|
||||
}
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
39
mobile/lib/utils/local_files_manager.dart
Normal file
39
mobile/lib/utils/local_files_manager.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LocalFilesManager {
|
||||
static const MethodChannel _channel = MethodChannel('file_trash');
|
||||
|
||||
static Future<bool> moveToTrash(String fileName) async {
|
||||
try {
|
||||
final bool success =
|
||||
await _channel.invokeMethod('moveToTrash', {'fileName': fileName});
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error moving to trash: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> restoreFromTrash(String fileName) async {
|
||||
try {
|
||||
final bool success = await _channel
|
||||
.invokeMethod('restoreFromTrash', {'fileName': fileName});
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error restoring file: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> requestManageStoragePermission() async {
|
||||
try {
|
||||
final bool success =
|
||||
await _channel.invokeMethod('requestManageStoragePermission');
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error requesting permission: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,51 @@
|
||||
import 'dart:async';
|
||||
// ignore_for_file: avoid-unsafe-collection-methods
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
const int targetVersion = 9;
|
||||
const int targetVersion = 10;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||
final int version = Store.get(StoreKey.version, 1);
|
||||
final int version = Store.get(StoreKey.version, targetVersion);
|
||||
|
||||
if (version < 9) {
|
||||
await Store.put(StoreKey.version, version);
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
final value = await db.storeValues.get(StoreKey.currentUser.id);
|
||||
if (value != null) {
|
||||
final id = value.intValue;
|
||||
if (id == null) {
|
||||
return;
|
||||
if (id != null) {
|
||||
await db.writeTxn(() async {
|
||||
final user = await db.users.get(id);
|
||||
await db.storeValues
|
||||
.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id));
|
||||
});
|
||||
}
|
||||
await db.writeTxn(() async {
|
||||
final user = await db.users.get(id);
|
||||
await db.storeValues
|
||||
.put(StoreValue(StoreKey.currentUser.id, strValue: user?.id));
|
||||
});
|
||||
}
|
||||
// Do not clear other entities
|
||||
return;
|
||||
}
|
||||
|
||||
if (version < targetVersion) {
|
||||
_migrateTo(db, targetVersion);
|
||||
if (version < 10) {
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
await _migrateDeviceAsset(db);
|
||||
}
|
||||
|
||||
final shouldTruncate = version < 8 && version < targetVersion;
|
||||
if (shouldTruncate) {
|
||||
await _migrateTo(db, targetVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,3 +60,59 @@ Future<void> _migrateTo(Isar db, int version) async {
|
||||
});
|
||||
await Store.put(StoreKey.version, version);
|
||||
}
|
||||
|
||||
Future<void> _migrateDeviceAsset(Isar db) async {
|
||||
final ids = Platform.isAndroid
|
||||
? (await db.androidDeviceAssets.where().findAll())
|
||||
.map((a) => _DeviceAsset(assetId: a.id.toString(), hash: a.hash))
|
||||
.toList()
|
||||
: (await db.iOSDeviceAssets.where().findAll())
|
||||
.map((i) => _DeviceAsset(assetId: i.id, hash: i.hash))
|
||||
.toList();
|
||||
final localAssets = (await db.assets
|
||||
.where()
|
||||
.anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId))
|
||||
.findAll())
|
||||
.map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt))
|
||||
.toList();
|
||||
debugPrint("Device Asset Ids length - ${ids.length}");
|
||||
debugPrint("Local Asset Ids length - ${localAssets.length}");
|
||||
ids.sort((a, b) => a.assetId.compareTo(b.assetId));
|
||||
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
|
||||
final List<DeviceAssetEntity> toAdd = [];
|
||||
await diffSortedLists(
|
||||
ids,
|
||||
localAssets,
|
||||
compare: (a, b) => a.assetId.compareTo(b.assetId),
|
||||
both: (deviceAsset, asset) {
|
||||
toAdd.add(
|
||||
DeviceAssetEntity(
|
||||
assetId: deviceAsset.assetId,
|
||||
hash: deviceAsset.hash!,
|
||||
modifiedTime: asset.dateTime!,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (deviceAsset) {
|
||||
debugPrint(
|
||||
'DeviceAsset not found in local assets: ${deviceAsset.assetId}',
|
||||
);
|
||||
},
|
||||
onlySecond: (asset) {
|
||||
debugPrint('Local asset not found in DeviceAsset: ${asset.assetId}');
|
||||
},
|
||||
);
|
||||
debugPrint("Total number of device assets migrated - ${toAdd.length}");
|
||||
await db.writeTxn(() async {
|
||||
await db.deviceAssetEntitys.putAll(toAdd);
|
||||
});
|
||||
}
|
||||
|
||||
class _DeviceAsset {
|
||||
final String assetId;
|
||||
final List<int>? hash;
|
||||
final DateTime? dateTime;
|
||||
|
||||
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:punycode/punycode.dart';
|
||||
|
||||
String sanitizeUrl(String url) {
|
||||
// Add schema if none is set
|
||||
@@ -11,13 +12,80 @@ String sanitizeUrl(String url) {
|
||||
}
|
||||
|
||||
String? getServerUrl() {
|
||||
final serverUrl = Store.tryGet(StoreKey.serverEndpoint);
|
||||
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
|
||||
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||
if (serverUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return serverUri.hasPort
|
||||
? "${serverUri.scheme}://${serverUri.host}:${serverUri.port}"
|
||||
: "${serverUri.scheme}://${serverUri.host}";
|
||||
return Uri.decodeFull(
|
||||
serverUri.hasPort
|
||||
? "${serverUri.scheme}://${serverUri.host}:${serverUri.port}"
|
||||
: "${serverUri.scheme}://${serverUri.host}",
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts a Unicode URL to its ASCII-compatible encoding (Punycode).
|
||||
///
|
||||
/// This is especially useful for internationalized domain names (IDNs),
|
||||
/// where parts of the URL (typically the host) contain non-ASCII characters.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final encodedUrl = punycodeEncodeUrl('https://bücher.de');
|
||||
/// print(encodedUrl); // Outputs: https://xn--bcher-kva.de
|
||||
/// ```
|
||||
///
|
||||
/// Notes:
|
||||
/// - If the input URL is invalid, an empty string is returned.
|
||||
/// - Only the host part of the URL is converted to Punycode; the scheme,
|
||||
/// path, and port remain unchanged.
|
||||
///
|
||||
String punycodeEncodeUrl(String serverUrl) {
|
||||
final serverUri = Uri.tryParse(serverUrl);
|
||||
if (serverUri == null || serverUri.host.isEmpty) return '';
|
||||
|
||||
final encodedHost = Uri.decodeComponent(serverUri.host).split('.').map(
|
||||
(segment) {
|
||||
// If segment is already ASCII, then return as it is.
|
||||
if (segment.runes.every((c) => c < 0x80)) return segment;
|
||||
return 'xn--${punycodeEncode(segment)}';
|
||||
},
|
||||
).join('.');
|
||||
|
||||
return serverUri.replace(host: encodedHost).toString();
|
||||
}
|
||||
|
||||
/// Decodes an ASCII-compatible (Punycode) URL back to its original Unicode representation.
|
||||
///
|
||||
/// This method is useful for converting internationalized domain names (IDNs)
|
||||
/// that were previously encoded with Punycode back to their human-readable Unicode form.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final decodedUrl = punycodeDecodeUrl('https://xn--bcher-kva.de');
|
||||
/// print(decodedUrl); // Outputs: https://bücher.de
|
||||
/// ```
|
||||
///
|
||||
/// Notes:
|
||||
/// - If the input URL is invalid the method returns `null`.
|
||||
/// - Only the host part of the URL is decoded. The scheme and port (if any) are preserved.
|
||||
/// - The method assumes that the input URL only contains: scheme, host, port (optional).
|
||||
/// - Query parameters, fragments, and user info are not handled (by design, as per constraints).
|
||||
///
|
||||
String? punycodeDecodeUrl(String? serverUrl) {
|
||||
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||
if (serverUri == null || serverUri.host.isEmpty) return null;
|
||||
|
||||
final decodedHost = serverUri.host.split('.').map(
|
||||
(segment) {
|
||||
if (segment.toLowerCase().startsWith('xn--')) {
|
||||
return punycodeDecode(segment.substring(4));
|
||||
}
|
||||
// If segment is not punycode encoded, then return as it is.
|
||||
return segment;
|
||||
},
|
||||
).join('.');
|
||||
|
||||
return Uri.decodeFull(serverUri.replace(host: decodedHost).toString());
|
||||
}
|
||||
|
||||
@@ -8,25 +8,25 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'asset_grid_data_structure.dart';
|
||||
@@ -107,6 +107,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
final Set<Asset> _draggedAssets =
|
||||
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||
|
||||
ScrollPhysics? _scrollPhysics;
|
||||
|
||||
Set<Asset> _getSelectedAssets() {
|
||||
return Set.from(_selectedAssets);
|
||||
}
|
||||
@@ -265,6 +267,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
),
|
||||
itemBuilder: _itemBuilder,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
physics: _scrollPhysics,
|
||||
itemScrollController: _itemScrollController,
|
||||
scrollOffsetController: _scrollOffsetController,
|
||||
itemCount: widget.renderList.elements.length +
|
||||
@@ -439,6 +442,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
|
||||
void _setDragStartIndex(AssetIndex index) {
|
||||
setState(() {
|
||||
_scrollPhysics = const ClampingScrollPhysics();
|
||||
_dragAnchorAssetIndex = index.rowIndex;
|
||||
_dragAnchorSectionIndex = index.sectionIndex;
|
||||
_dragging = true;
|
||||
@@ -446,6 +450,12 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -546,15 +556,16 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
if (didPop) {
|
||||
return;
|
||||
} else {
|
||||
if (widget.preselectedAssets == null) {
|
||||
Navigator.of(context).canPop() ? Navigator.of(context).pop() : null;
|
||||
}
|
||||
if (_selectedAssets.length != widget.preselectedAssets!.length &&
|
||||
!widget.preselectedAssets!.containsAll(_selectedAssets)) {
|
||||
{
|
||||
_deselectAll();
|
||||
return;
|
||||
}
|
||||
/// `preselectedAssets` is only present when opening the asset grid from the
|
||||
/// "add to album" button.
|
||||
///
|
||||
/// `_selectedAssets` includes `preselectedAssets` on initialization.
|
||||
if (_selectedAssets.length >
|
||||
(widget.preselectedAssets?.length ?? 0)) {
|
||||
/// `_deselectAll` only deselects the selected assets,
|
||||
/// doesn't affect the preselected ones.
|
||||
_deselectAll();
|
||||
return;
|
||||
} else {
|
||||
Navigator.of(context).canPop() ? Navigator.of(context).pop() : null;
|
||||
}
|
||||
|
||||
@@ -34,17 +34,24 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
final owner = ref.watch(currentUserProvider);
|
||||
final hasError = useState(false);
|
||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||
final hasDescription = useState(false);
|
||||
final isOwner = fastHash(owner?.id ?? '') == asset.ownerId;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
assetService
|
||||
.getDescription(asset)
|
||||
.then((value) => controller.text = value);
|
||||
assetService.getDescription(asset).then((value) {
|
||||
controller.text = value;
|
||||
hasDescription.value = value.isNotEmpty;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
[assetWithExif.value],
|
||||
);
|
||||
|
||||
if (!isOwner && !hasDescription.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
submitDescription(String description) async {
|
||||
hasError.value = false;
|
||||
try {
|
||||
@@ -82,7 +89,7 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return TextField(
|
||||
enabled: fastHash(owner?.id ?? '') == asset.ownerId,
|
||||
enabled: isOwner,
|
||||
focusNode: focusNode,
|
||||
onTap: () => isFocus.value = true,
|
||||
onChanged: (value) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,18 +8,18 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/provider_utils.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/email_input.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/loading_icon.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/login_button.dart';
|
||||
@@ -82,7 +83,8 @@ class LoginForm extends HookConsumerWidget {
|
||||
/// Fetch the server login credential and enables oAuth login if necessary
|
||||
/// Returns true if successful, false otherwise
|
||||
Future<void> getServerAuthSettings() async {
|
||||
final serverUrl = sanitizeUrl(serverEndpointController.text);
|
||||
final sanitizeServerUrl = sanitizeUrl(serverEndpointController.text);
|
||||
final serverUrl = punycodeEncodeUrl(sanitizeServerUrl);
|
||||
|
||||
// Guard empty URL
|
||||
if (serverUrl.isEmpty) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
@@ -25,10 +27,14 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
|
||||
final advancedTroubleshooting =
|
||||
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
|
||||
final manageLocalMediaAndroid =
|
||||
useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||
final allowSelfSignedSSLCert =
|
||||
useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert);
|
||||
final useAlternatePMFilter =
|
||||
useAppSettingsState(AppSettingsEnum.photoManagerCustomFilter);
|
||||
|
||||
final logLevel = Level.LEVELS[levelId.value].name;
|
||||
|
||||
@@ -38,6 +44,16 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
||||
);
|
||||
|
||||
Future<bool> checkAndroidVersion() async {
|
||||
if (Platform.isAndroid) {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
int sdkVersion = androidInfo.version.sdkInt;
|
||||
return sdkVersion >= 30;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final advancedSettings = [
|
||||
SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
@@ -45,6 +61,29 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
title: "advanced_settings_troubleshooting_title".tr(),
|
||||
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: checkAndroidVersion(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
valueNotifier: manageLocalMediaAndroid,
|
||||
title: "advanced_settings_sync_remote_deletions_title".tr(),
|
||||
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
|
||||
onChanged: (value) async {
|
||||
if (value) {
|
||||
final result = await ref
|
||||
.read(localFilesManagerRepositoryProvider)
|
||||
.requestManageStoragePermission();
|
||||
manageLocalMediaAndroid.value = result;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
SettingsSliderListTile(
|
||||
text: "advanced_settings_log_level_title".tr(args: [logLevel]),
|
||||
valueNotifier: levelId,
|
||||
@@ -68,6 +107,12 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
),
|
||||
const CustomeProxyHeaderSettings(),
|
||||
SslClientCertSettings(isLoggedIn: ref.read(currentUserProvider) != null),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useAlternatePMFilter,
|
||||
title: "advanced_settings_enable_alternate_media_filter_title".tr(),
|
||||
subtitle:
|
||||
"advanced_settings_enable_alternate_media_filter_subtitle".tr(),
|
||||
),
|
||||
];
|
||||
|
||||
return SettingsSubPageScaffold(settings: advancedSettings);
|
||||
|
||||
@@ -36,7 +36,7 @@ class UserAdminCreateDto {
|
||||
|
||||
String password;
|
||||
|
||||
/// Minimum value: 1
|
||||
/// Minimum value: 0
|
||||
int? quotaSizeInBytes;
|
||||
|
||||
///
|
||||
|
||||
@@ -45,7 +45,7 @@ class UserAdminUpdateDto {
|
||||
///
|
||||
String? password;
|
||||
|
||||
/// Minimum value: 1
|
||||
/// Minimum value: 0
|
||||
int? quotaSizeInBytes;
|
||||
|
||||
///
|
||||
|
||||
@@ -463,7 +463,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user