Compare commits

..

43 Commits

Author SHA1 Message Date
Alex The Bot
b42ca61e1f Version v1.78.0 2023-09-13 08:24:02 +00:00
Alex
197baf3473 chore(mobile): update translation 2023-09-13 11:40:59 +07:00
Alex
3161eb7d16 chore(mobile): pump Flutter version (#4078) 2023-09-13 11:21:46 +07:00
Alex
bbbdd463fd chore(mobile): translation update (#4077) 2023-09-13 10:55:20 +07:00
shenlong
73ad0d468f feat(mobile): upload image assets before videos (#3958)
* feat(mobile): upload image assets before videos (#3872)

* feat(mobile): upload image assets before videos

* mobile: sort by creation date before uploading assets

* feat(mobile): upload newest assets first for foreground upload

* feat(mobile): upload images before videos only for background backup
2023-09-13 10:50:16 +07:00
Jason Rasmussen
74d34b4f6c refactor(server): android motion photos (#3711) 2023-09-13 10:46:37 +07:00
Louis-Marie Michelin
bf3b38a7f2 feat(web): load original image when zooming (#4040)
* feat(web): change max zoom from 4 to 10

* feat(web): load original image when zooming
2023-09-12 22:42:41 +07:00
Daniel Dietzler
52d0c5fc73 fix timeline overflowing dropdowns, more resonable z-indices (#4071) 2023-09-12 10:52:32 -04:00
Dhrumil Shah
fb20381f98 feat(mobile): allow self-signed certificate on the mobile app (#4051)
* WIP: self-signed certs accept

* WIP: format

* WIP: pushing up adding settings menu

* Add serverEndpointURL check

* Add translation update

* Handle errors properly

* format

* typo

* cleanup

* styling and permission

* remove deadcode

* put pack condition

* styling

* remove hiding settings options

* format + match drop shadow

* match color

* remove deadcode

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-09-12 21:51:43 +07:00
martin
a678590ccd refactor(web): disable shortcut when writting (#4057)
* Revert "fix: disable shortcut when writting text (#4053)"

This reverts commit fd6ade2b5d.

* refactor: disable shortcut when writting

* pr feedback

* pr feedback
2023-09-12 21:26:53 +07:00
Fynn Petersen-Frey
bd226e9e2c fix(mobile): images rendered twice due to rebuild (#4060)
* fix(mobile): images rendered twice due to rebuild

* fix bottom sheet triggered multiple times
2023-09-12 20:43:15 +07:00
dependabot[bot]
d023d5b6b4 chore(deps): bump docker/setup-buildx-action from 2.10.0 to 3.0.0 (#4066)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.10.0 to 3.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.10.0...v3.0.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 08:23:14 -04:00
dependabot[bot]
9b30640e67 chore(deps): bump docker/setup-qemu-action from 2.2.0 to 3.0.0 (#4067)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.2.0 to 3.0.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.2.0...v3.0.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 08:22:37 -04:00
dependabot[bot]
d17b24eea3 chore(deps): bump docker/build-push-action from 4.2.1 to 5.0.0 (#4068)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.2.1 to 5.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4.2.1...v5.0.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 08:17:18 -04:00
dependabot[bot]
d38d0b8de0 chore(deps): bump docker/login-action from 2 to 3 (#4069)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 08:16:26 -04:00
dependabot[bot]
f10b74f1e2 chore(deps): bump docker/metadata-action from 4 to 5 (#4070)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 08:15:16 -04:00
bo0tzz
5c63d8f07a format(docs) (#4064)
* fix(docs): Link typo

* fix(docs): Footnote whitespace

* format(docs)
2023-09-12 15:30:15 +07:00
Mert
cb437829f3 chore(docs): updated ML documentation (#4063) 2023-09-12 13:22:42 +07:00
Daniel Dietzler
7173af60e4 chore(server): Improve test coverage! (#3889)
* tests for person service

* tests for auth service

* tests for access core

* improve tests for album service

* fix missing brackets and remove comments

* tests for asset service

* tests for face recognition

* tests for job service

* feedback

* tests for search service (broken)

* fix: disabled search test

* tests for smart-info service

* tests for storage template service

* tests for user service

* fix formatting of untouched files LOL

* attempt to fix formatting

* streamline api utils, add asset api for uploading files

* test upload of assets

* fix formatting

* move test-utils to correct folder

* test add assets to album

* use random bytes instead of test image

* (e2e) test albums with assets

* (e2e) complete tests for album endpoints

* (e2e) tests for asset endpoint

* fix: asset upload/import dto validation

* (e2e) tests for statistics asset endpoint

* fix wrong describe text

* (e2e) tests for people with faces

* (e2e) clean up person tests

* (e2e) tests for partner sharing endpoints

* (e2e) tests for link sharing

* (e2e) tests for the asset time bucket endpoint

* fix minor issues

* remove access.core.spec.ts

* chore: wording

* chore: organize test api files

* chore: fix test describe

* implement feedback

* fix race condition in album tests

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-09-11 11:56:38 -04:00
Jason Rasmussen
afccb37a3b fix(web): album selection modal overflow and alignment (#4059) 2023-09-11 22:55:21 +07:00
dependabot[bot]
c55ef7c383 chore(deps): bump docker/build-push-action from 4.2.0 to 4.2.1 (#4055)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 07:38:30 -04:00
Alex
47ea47ce14 fix(mobile): Fix background backup on iOS (#4038) 2023-09-11 17:31:15 +07:00
martin
fd6ade2b5d fix: disable shortcut when writting text (#4053) 2023-09-11 17:28:41 +07:00
Daniel Dietzler
77e38abe91 chore: update env docs to, indicate that a container restart is necessary (#4054) 2023-09-11 17:16:03 +07:00
Fynn Petersen-Frey
5d1011b482 feat(mobile): efficient asset sync (#3945)
* feat(mobile): efficient asset sync
2023-09-10 14:51:18 +02:00
Mert
4b11e925d9 fix(server): handle failed ML responses (#4036)
* handle ml error responses

* more explicit error message

* formatting

* better formatting
2023-09-09 16:03:59 +07:00
Mert
258b98c262 fix(ml): load models in separate threads (#4034)
* load models in thread

* set clip mode logs to debug level

* updated tests

* made fixtures slightly less ugly

* moved responses to json file

* formatting
2023-09-09 16:02:44 +07:00
Jason Rasmussen
f1db257628 feat(server,web): server config (#4006)
* feat: server config

* chore: open api

* fix: redirect /map to /photos when disabled
2023-09-08 22:51:46 -04:00
Jonathan Jogenfors
3edade6761 feat(server): tsconfig: don't clear console on nest startup (#4032)
* feat: don't clear nest output

* feat: moved config to tsconfig

* fix: cleanup package json
2023-09-08 11:17:45 -04:00
Jonathan Jogenfors
efcc66d63b feat(web): only log http errors in web container (#4031)
* feat: reduce web container log verbosity on error

* fix: web test

* feat: better error logging

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-09-08 15:09:59 +00:00
dependabot[bot]
ca96da22d0 chore(deps): bump docker/build-push-action from 4.1.1 to 4.2.0 (#4027)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4.1.1 to 4.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4.1.1...v4.2.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-08 10:26:42 -04:00
Damien
85b98cf4c6 Update FAQ.md (#4030) 2023-09-08 20:17:16 +07:00
Jonathan Jogenfors
a404fb6cb5 feat: don't clear nest output (#4023) 2023-09-08 14:45:00 +07:00
Daniele Ricci
3432b4625f fix(server): regenerate missing person thumbnails (#3970)
* Regenerate missing person thumbnails

* Check for empty string instead of zero length

* Remember asset used as person face

* Define entity relation between person and asset via faceAssetId

* Typo

* Fix entity relation

* Tests

* Tests

* Fix code formatting

* Fix import path

* Fix migration

* format

* Fix entity and migration

* Linting

* Remove unneeded cast

* Conventions

* Simplify queries

* Simplify queries

* Remove unneeded typings from entity

* Remove unneeded cast

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-09-08 13:49:43 +07:00
Alexandre Bouijoux
b8777d7739 Add french documentation (#4010)
* Copy original README for online editing

* Translate to french

* Add link from other documentations

* Add machine-learning french doc

* Typos

* Missing word

---------

Co-authored-by: Alexandre Bouijoux <alexandre@bouijoux.fr>
2023-09-08 13:48:39 +07:00
Dhrumil Shah
fb477627c7 fix(mobile): Clean up image details if there is missing info (#4018)
* fix 0,0 location

* image details cleanup

* Static anal fix
2023-09-08 12:55:55 +07:00
noamfuss
fd78b89c92 Update README.md (#4016)
Corrected map support on mobile
2023-09-07 22:40:24 +00:00
magicedy
45f9c52e7f chore(mobile): build split APKs (#3940)
* chore(mobile): build split APKs

* chore(mobile): build split APKs and universalApk
2023-09-07 09:49:53 +07:00
Mert
0a24ff90bb fix(ml): set higher default worker timeout (#4007) 2023-09-07 08:27:29 +07:00
Dhrumil Shah
e1eae00b35 Truncate Log lines (#4003) 2023-09-07 03:53:11 +07:00
Jason Rasmussen
15bfceb05a fix(server): update asset with tagged people (#4000) 2023-09-07 03:51:09 +07:00
dependabot[bot]
c1f4fe65bb chore(deps): bump stumpylog/image-cleaner-action from 0.2.0 to 0.3.0 (#3997)
Bumps [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.2.0...v0.3.0)

---
updated-dependencies:
- dependency-name: stumpylog/image-cleaner-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 15:18:42 -04:00
Jason Rasmussen
a4a6a97aa8 chore: skip mobile app build for dependabot (#4001) 2023-09-06 18:01:51 +00:00
197 changed files with 6320 additions and 1736 deletions

View File

@@ -19,7 +19,7 @@ jobs:
build-sign-android:
name: Build and sign Android
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
runs-on: macos-12
steps:
@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.0"
flutter-version: "3.13.3"
cache: true
- name: Create the Keystore
@@ -64,10 +64,12 @@ jobs:
ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
run: flutter build apk --release
run: |
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
- name: Publish Android Artifact
uses: actions/upload-artifact@v3
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/app-release.apk
path: mobile/build/app/outputs/flutter-apk/*.apk

View File

@@ -38,7 +38,7 @@ jobs:
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.2.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
@@ -70,7 +70,7 @@ jobs:
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.2.0
uses: stumpylog/image-cleaner-action/untagged@v0.3.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"

View File

@@ -39,10 +39,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.10.0
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
@@ -53,13 +53,13 @@ jobs:
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
@@ -69,7 +69,7 @@ jobs:
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
@@ -97,7 +97,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.0.0
with:
context: ${{ matrix.context }}
platforms: ${{ matrix.platforms }}
@@ -123,10 +123,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.10.0
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
@@ -137,13 +137,13 @@ jobs:
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
@@ -153,7 +153,7 @@ jobs:
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
@@ -181,7 +181,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.0.0
with:
context: ${{ matrix.context }}
platforms: ${{ matrix.platforms }}

View File

@@ -23,7 +23,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.0"
flutter-version: "3.13.3"
- name: Install dependencies
run: dart pub get

View File

@@ -149,7 +149,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.13.0"
flutter-version: "3.13.3"
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1

View File

@@ -22,6 +22,7 @@
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
</p>
## Disclaimer
@@ -85,7 +86,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| User-defined storage structure | Yes | Yes |
| Public Sharing | No | Yes |
| Archive and Favorites | Yes | Yes |
| Global Map | No | Yes |
| Global Map | Yes | Yes |
| Partner Sharing | Yes | Yes |
| Facial recognition and clustering | Yes | Yes |
| Memories (x years ago) | Yes | Yes |

View File

@@ -22,6 +22,7 @@
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
</p>
## Avís legal

View File

@@ -22,6 +22,7 @@
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_fr_FR.md">Français</a>
</p>
## Descargo de responsabilidad

110
README_fr_FR.md Normal file
View File

@@ -0,0 +1,110 @@
<p align="center">
<br/>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
<a href="https://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
</a>
<br/>
<br/>
</p>
<p align="center">
<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
</p>
<h3 align="center">Immich - Solution de sauvegarde performante et auto-hébergée des photos et des vidéos</h3>
<br/>
<a href="https://immich.app">
<img src="design/immich-screenshots.png" title="Main Screenshot">
</a>
<br/>
<p align="center">
<a href="README_zh_CN.md">中文</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
</p>
## Clause de non-responsabilité
- ⚠️ Le projet est en **très fort** développement.
- ⚠️ Attendez-vous à rencontrer des bugs et des changements importants.
- ⚠️ **N'utilisez pas cette application comme seule façon de sauvegarder vos photos et vos vidéos.**
- ⚠️ Ayez toujours un plan de sauvegarde en [3-2-1](https://www.seagate.com/fr/fr/blog/what-is-a-3-2-1-backup-strategy/) pour vos précieuses photos et vidéos !
## Sommaire
- [Documentation officielle](https://immich.app/docs)
- [Feuille de route](https://github.com/orgs/immich-app/projects/1)
- [Démo](#demo)
- [Fonctionnalités](#features)
- [Introduction](https://immich.app/docs/overview/introduction)
- [Installation](https://immich.app/docs/install/requirements)
- [Contribution](https://immich.app/docs/overview/support-the-project)
- [Soutenir le projet](#support-the-project)
## Documentation
Vous pouvez trouver la documentation principale ainsi que les guides d'installation sur https://immich.app/.
## Démo
Vous pouvez accéder à la démo Web sur https://demo.immich.app
Pour l'application mobile, vous pouvez utiliser `https://demo.immich.app/api` dans le champ 'URL du point d'accès au serveur'
```bash title="Demo Credential"
Les identifiants
email: demo@immich.app
mot de passe: demo
```
```
Caractéristiques: Plan gratuit Oracle VM - Amsterdam - 2.4Ghz quatre-cœurs ARM64 CPU, 24GB RAM
```
# Fonctionnalités
| Fonctionnalités | Mobile | Web |
| ---------------------------------------------------------------- | ------ | --- |
| Téléverser et voir les vidéos et photos | Oui | Oui |
| Sauvegarde automatique quand l'application est ouverte | Oui | N/A |
| Sélection des albums à sauvegarder | Oui | N/A |
| Télécharger les photos et les vidéos sur l'appareil | Oui | Oui |
| Support multi-utilisateur | Oui | Oui |
| Albums et albums partagés | Oui | Oui |
| Barre de défilement mobile | Oui | Oui |
| Support des formats raw | Oui | Oui |
| Vue sur les métadonnées (EXIF, carte) | Oui | Oui |
| Rechercher par métadonnées, objets, faces et CLIP | Oui | Oui |
| Fonctions d'administration (gestion des utilisateurs) | Non | Oui |
| Sauvegarde en tâche de fond | Oui | N/A |
| Défilement virtuel | Oui | Oui |
| Support de l'OAuth | Oui | Oui |
| Clés d'API | N/A | Oui |
| Sauvegarde et lecture des LivePhotos | iOS | Oui |
| Structure de stockage définissable | Oui | Oui |
| Partage public | Non | Oui |
| Archives et favoris | Oui | Oui |
| Carte globale | Non | Oui |
| Partage entre utilisateurs | Oui | Oui |
| Reconnaissance et regroupement facial | Oui | Oui |
| Souvenirs (il y a x années) | Oui | Oui |
| Support hors-ligne | Oui | Non |
| Gallerie en lecture seule | Oui | Oui |
# Soutenir le projet
Je me suis engagé sur ce projet, et je ne compte pas m'arrêter. Je continuerai à mettre à jour les documentations, d'ajouter de nouvelles fonctionnalités et de résoudre des bugs. Mais je ne peux pas faire cela seul. Donc j'ai besoin de votre aide pour me donner encore plus de motivation et ainsi continuer.
Comme l'ont dit nos hôtes dans le [selfhosted.show - Dans l'épisode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418), c'est un travail colossal ce que l'équipe et moi faisons. J'aimerais un jour être capable de faire ça à temps plein, c'est pourquoi je vous demande votre aide pour rendre cela possible.
Si vous estimez que c'est pour la bonne cause et que vous prévoyez d'utiliser l'application pour un moment, s'il-vous-plaît, pensez à soutenir le projet avec les moyens ci-dessous.
## Donation
- [Donation mensuelle](https://github.com/sponsors/alextran1502) via GitHub Sponsors
- [Donation occasionnelle](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
- [Librepay](https://liberapay.com/alex.tran1502/)
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

View File

@@ -22,6 +22,7 @@
<a href="README_zh_CN.md">中文</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
</p>
## Feragatname

View File

@@ -26,6 +26,7 @@
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
</p>

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.77.0
* The version of the OpenAPI document: 1.78.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -2343,6 +2343,31 @@ export interface SearchResponseDto {
*/
'assets': SearchAssetResponseDto;
}
/**
*
* @export
* @interface ServerConfigDto
*/
export interface ServerConfigDto {
/**
*
* @type {string}
* @memberof ServerConfigDto
*/
'loginPageMessage': string;
/**
*
* @type {string}
* @memberof ServerConfigDto
*/
'mapTileUrl': string;
/**
*
* @type {string}
* @memberof ServerConfigDto
*/
'oauthButtonText': string;
}
/**
*
* @export
@@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto {
* @memberof ServerFeaturesDto
*/
'facialRecognition': boolean;
/**
*
* @type {boolean}
* @memberof ServerFeaturesDto
*/
'map': boolean;
/**
*
* @type {boolean}
@@ -2810,6 +2841,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'machineLearning': SystemConfigMachineLearningDto;
/**
*
* @type {SystemConfigMapDto}
* @memberof SystemConfigDto
*/
'map': SystemConfigMapDto;
/**
*
* @type {SystemConfigOAuthDto}
@@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto {
*/
'url': string;
}
/**
*
* @export
* @interface SystemConfigMapDto
*/
export interface SystemConfigMapDto {
/**
*
* @type {boolean}
* @memberof SystemConfigMapDto
*/
'enabled': boolean;
/**
*
* @type {string}
* @memberof SystemConfigMapDto
*/
'tileUrl': string;
}
/**
*
* @export
@@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI {
*/
export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/server-info/config`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
@@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
export const ServerInfoApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getServerConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ServerConfigDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getServerConfig(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
@@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) {
export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ServerInfoApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getServerConfig(options?: AxiosRequestConfig): AxiosPromise<ServerConfigDto> {
return localVarFp.getServerConfig(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
@@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas
* @extends {BaseAPI}
*/
export class ServerInfoApi extends BaseAPI {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ServerInfoApi
*/
public getServerConfig(options?: AxiosRequestConfig) {
return ServerInfoApiFp(this.configuration).getServerConfig(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.77.0
* The version of the OpenAPI document: 1.78.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.77.0
* The version of the OpenAPI document: 1.78.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.77.0
* The version of the OpenAPI document: 1.78.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.77.0
* The version of the OpenAPI document: 1.78.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -39,15 +39,40 @@ This often happens when using a reverse proxy or cloudflare tunnel in front of I
### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_ENABLED=false` in your .env file.
Immich optionally uses machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#how-can-i-lower-immichs-cpu-usage) this or [disable](/docs/FAQ.md#how-can-i-disable-machine-learning) machine learning entirely.
### How to disable machine-learning and TypeSense?
### How can I lower Immich's CPU usage?
:::warning
Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning.
The initial backup is the most intensive due to the number of jobs running. The most CPU-intensive ones are transcoding and machine learning jobs (Tag Images, Encode CLIP, Recognize Faces), and to a lesser extent thumbnail generation. Here are some ways to lower their CPU usage:
- Lower the job concurrency for these jobs to 1.
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
- Set the `TYPESENSE_THREAD_POOL_SIZE` environmental variable and restart the Typesense container. For instance, `TYPESENSE_THREAD_POOL_SIZE=8` will limit it to 8 threads.
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
- You _must_ re-run the Recognize Faces job for all images after this for facial recognition on new images to work properly.
- If these changes are not enough, see [below](/docs/FAQ.md#how-can-i-disable-machine-learning) for how you can disable machine learning.
### How can I disable machine learning?
:::info
Disabling machine learning will result in a poor experience for searching and the 'Explore' page, as these are reliant on it to work as intended.
:::
These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_ENABLED=false` & `TYPESENSE_ENABLED=false` in your .env file.
Machine learning can be disabled under Settings > Machine Learning Settings, either entirely or by model type. For instance, you can choose to disable smart search with CLIP, but keep facial recognition enabled. This means that the machine learning service will only process the enabled jobs.
However, disabling all jobs will not disable the machine learning service itself. To prevent it from starting up at all in this case, you can comment out the `immich-machine-learning` section of the docker-compose.yml.
### How can I disable TypeSense?
:::info
Disabling Typesense will result in a poor search experience since searching is reliant on it.
:::
You can disable Typesense by commenting out the `immich-typesense` section of the docker-compose.yml and setting `TYPESENSE_ENABLED=false` in your .env file.
### I'm getting errors about models being corrupt or failing to download. What do I do?
You can delete the model cache volume, which is where models are downloaded. This will give the service a clean environment to download the model again.
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
@@ -59,7 +84,7 @@ This is fixed by running the storage migration job.
### Why is object detection not very good?
The model we used for machine learning is a prebuilt model, so the accuracy is not very good. It will hopefully be replaced with a better solution in the future.
The default image tagging model is relatively small. You can change this for a larger model like `google/vit-base-patch16-224` by setting the model name under Settings > Machine Learning Settings > Image Tagging. You can then re-run the Image Tagging job to get improved tags.
### How can I see Immich logs?
@@ -96,10 +121,6 @@ docker-compose down -v
After removing the containers and volumes, the **Files** can be cleaned up (if necessary) from the `UPLOAD_LOCATION` by simply deleting an unwanted files or folders.
### Why iOS app shows duplicate photos on the timeline while the web doesn't?
If you are using `My Photo Stream`, the Photos app temporarily creates duplicates of photos taken in the last 30 days. These photos are included in the `Recents` album and thus shown up twice. To fix this, you can disable `My Photo Stream` in the native Photos app or choose a different album in the backup screen in Immich.
### How can I move all data (photos, persons, albums) from one user to another?
This requires some database queries. You can do this on the command line (in the PostgreSQL container using the psql command), or you can add for example an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file, so that you can use a web-interface.

View File

@@ -89,6 +89,12 @@ The machine learning service is written in [Python](https://www.python.org/) and
All machine learning related operations have been externalized to this service, `immich-machine-learning`. Python is a natural choice for AI and machine learning. It also has some pretty specific hardware requirements. Running it as a separate container makes it possible to run the container on a separate machine, or easily disable it entirely.
Each request to the machine learning service contains the relevant metadata for the model task, model name, and so on. These settings are stored in Postgres along with other system configs. For each request, the microservices container fetches these settings in order to attach them to the request.
Internally, the machine learning service downloads, loads and configures the specified model for a given request before processing the text or image payload with it. Models that have been loaded are cached and reused across requests. A thread pool is used to process each request in a different thread so as not to block the async event loop.
All models are in ONNX format. This format has wide industry support, meaning that most other model formats can be exported to it and many hardware APIs support it. It's also quite fast.
Machine learning models are also quite _large_, requiring _quite a bit_ of memory. We are always looking for ways to improve and optimize this aspect of this container specifically.
### Postgres

View File

@@ -54,6 +54,25 @@ The default configuration looks like this:
"concurrency": 1
}
},
"machineLearning": {
"classification": {
"minScore": 0.7,
"enabled": true,
"modelName": "microsoft/resnet-50"
},
"enabled": true,
"url": "http://immich-machine-learning:3003",
"clip": {
"enabled": true,
"modelName": "ViT-B-32::openai"
},
"facialRecognition": {
"enabled": true,
"modelName": "buffalo_l",
"minScore": 0.7,
"maxDistance": 0.6
}
},
"oauth": {
"enabled": false,
"issuerUrl": "",
@@ -75,7 +94,9 @@ The default configuration looks like this:
},
"thumbnail": {
"webpSize": 250,
"jpegSize": 1440
"jpegSize": 1440,
"quality": 90,
"colorspace": "p3"
}
}
```

View File

@@ -4,6 +4,17 @@ sidebar_position: 90
# Environment Variables
:::caution
To change environment variables, you must recreate the Immich containers.
Just restarting the containers does not replace the environment within the container!
In order to recreate the container using docker compose, run `docker compose up -d`.
In most cases docker will recognize that the `.env` file has changed and recreate the affected containers.
If this should not work, try running `docker compose up -d --force-recreate`.
:::
## Docker Compose
| Variable | Description | Default | Services |
@@ -177,15 +188,21 @@ Typesense URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Services |
| :----------------------------------------------- | :----------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Model TTL | `300` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | ML Cache Location | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Request thread pool size | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| Variable | Description | Default | Services |
| :----------------------------------------------- | :---------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL`<sup>\*1</sup> | Inactivity time (s) before a model is unloaded (disabled if <= 0) | `0` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*2</sup> | Thread count of the request thread pool (disabled if <= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*3</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
\*1: This is an experimental feature. It may result in increased memory use over time when loading models repeatedly.
\*2: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
\*3: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around.
:::info

View File

@@ -17,6 +17,8 @@ Be sure to commit the `poetry.lock` and `pyproject.toml` files to reflect any ch
To measure inference throughput and latency, you can use [Locust](https://locust.io/) using the provided `locustfile.py`.
Locust works by querying the model endpoints and aggregating their statistics, meaning the app must be deployed.
You can run `load_test.sh` to automatically deploy the app locally and start Locust, optionally adjusting its env variables as needed.
You can change the models or adjust options like score thresholds through the Locust UI.
Alternatively, for more custom testing, you may also run `locust` directly: see the [documentation](https://docs.locust.io/en/stable/index.html). Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.
To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust.
Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.

View File

@@ -0,0 +1,22 @@
# Immich Apprentissage machine
- Classification d'images
- Embarquement de CLIP
- Reconnaissance faciale
# Mise en place
Ce projet utilise [Poetry](https://python-poetry.org/docs/#installation), donc soyez certain de l'installer en premier.
Exécuter `poetry install --no-root --with dev` installera tout ce dont vous avez besoin dans un environnement virtuel isolé.
Pour ajouter ou supprimer des dépendances, vous pouvez utiliser les commandes `poetry add $PACKAGE_NAME` et `poetry remove $PACKAGE_NAME` respectivement.
Soyez sûr de commit les fichiers `poetry.lock` et `pyproject.toml` pour refléter les changements de dépendances.
# Test de charge
Pour mesurer le débit d'inférence et la latence, vous pouvez utiliser [Locust](https://locust.io/) avec le fichier fourni `locustfile.py`.
Locust fonctionne en interrogeant les endpoints des modèles et en aggrégeant leurs statistiques, signifiant que l'application doit être déployée.
Vous pouvez exécuter `load_test.sh` pour automatiquement déployer l'application localement et démarrer Locust, en ajustant si besoin ses variables d'environnement.
En alternative, pour réaliser plus de tests customisés, vous pourriez aussi exécuter `locust` directement : voir la [documentation](https://docs.locust.io/en/stable/index.html). Notez que dans le jargon de Locust, la concurrence est mesurée en `users` et que chaque user exécute une tâche après l'autre. Pour parvenir à une concurrence par endpoint, multipliez ce nombre par le nombre d'endpoints à interroger. Par exemple, s'il y a 3 endpoints et que vous voulez que chacun d'entre eux reçoive 8 requêtes à la fois, vous devrez mettre ce nombre d'users à 24.

View File

@@ -1,4 +1,5 @@
from typing import Iterator, TypeAlias
import json
from typing import Any, Iterator, TypeAlias
from unittest import mock
import numpy as np
@@ -31,3 +32,8 @@ def mock_get_model() -> Iterator[mock.Mock]:
def deployed_app() -> TestClient:
init_state()
return TestClient(app)
@pytest.fixture(scope="session")
def responses() -> dict[str, Any]:
return json.load(open("responses.json", "r"))

View File

@@ -1,10 +1,13 @@
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from zipfile import BadZipFile
import orjson
from fastapi import FastAPI, Form, HTTPException, UploadFile
from fastapi.responses import ORJSONResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile # type: ignore
from starlette.formparsers import MultiPartParser
from app.models.base import InferenceModel
@@ -31,6 +34,7 @@ def init_state() -> None:
)
# asyncio is a huge bottleneck for performance, so we use a thread pool to run blocking code
app.state.thread_pool = ThreadPoolExecutor(settings.request_threads) if settings.request_threads > 0 else None
app.state.locks = {model_type: threading.Lock() for model_type in ModelType}
log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
@@ -63,14 +67,49 @@ async def predict(
inputs = text
else:
raise HTTPException(400, "Either image or text must be provided")
try:
kwargs = orjson.loads(options)
except orjson.JSONDecodeError:
raise HTTPException(400, f"Invalid options JSON: {options}")
model: InferenceModel = await app.state.model_cache.get(model_name, model_type, **orjson.loads(options))
model = await load(await app.state.model_cache.get(model_name, model_type, **kwargs))
model.configure(**kwargs)
outputs = await run(model, inputs)
return ORJSONResponse(outputs)
async def run(model: InferenceModel, inputs: Any) -> Any:
if app.state.thread_pool is not None:
return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs)
else:
if app.state.thread_pool is None:
return model.predict(inputs)
return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs)
async def load(model: InferenceModel) -> InferenceModel:
if model.loaded:
return model
def _load() -> None:
with app.state.locks[model.model_type]:
model.load()
loop = asyncio.get_running_loop()
try:
if app.state.thread_pool is None:
model.load()
else:
await loop.run_in_executor(app.state.thread_pool, _load)
return model
except (OSError, InvalidProtobuf, BadZipFile, NoSuchFile):
log.warn(
(
f"Failed to load {model.model_type.replace('_', ' ')} model '{model.model_name}'."
"Clearing cache and retrying."
)
)
model.clear_cache()
if app.state.thread_pool is None:
model.load()
else:
await loop.run_in_executor(app.state.thread_pool, _load)
return model

View File

@@ -5,10 +5,8 @@ from abc import ABC, abstractmethod
from pathlib import Path
from shutil import rmtree
from typing import Any
from zipfile import BadZipFile
import onnxruntime as ort
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile # type: ignore
from ..config import get_cache_dir, log, settings
from ..schemas import ModelType
@@ -21,16 +19,13 @@ class InferenceModel(ABC):
self,
model_name: str,
cache_dir: Path | str | None = None,
eager: bool = True,
inter_op_num_threads: int = settings.model_inter_op_threads,
intra_op_num_threads: int = settings.model_intra_op_threads,
**model_kwargs: Any,
) -> None:
self.model_name = model_name
self._loaded = False
self.loaded = False
self._cache_dir = Path(cache_dir) if cache_dir is not None else get_cache_dir(model_name, self.model_type)
loader = self.load if eager else self.download
self.providers = model_kwargs.pop("providers", ["CPUExecutionProvider"])
# don't pre-allocate more memory than needed
self.provider_options = model_kwargs.pop(
@@ -55,34 +50,23 @@ class InferenceModel(ABC):
self.sess_options.intra_op_num_threads = intra_op_num_threads
self.sess_options.enable_cpu_mem_arena = False
try:
loader(**model_kwargs)
except (OSError, InvalidProtobuf, BadZipFile, NoSuchFile):
log.warn(
(
f"Failed to load {self.model_type.replace('_', ' ')} model '{self.model_name}'."
"Clearing cache and retrying."
)
)
self.clear_cache()
loader(**model_kwargs)
def download(self, **model_kwargs: Any) -> None:
def download(self) -> None:
if not self.cached:
log.info(
(f"Downloading {self.model_type.replace('_', ' ')} model '{self.model_name}'." "This may take a while.")
(f"Downloading {self.model_type.replace('-', ' ')} model '{self.model_name}'." "This may take a while.")
)
self._download(**model_kwargs)
self._download()
def load(self, **model_kwargs: Any) -> None:
self.download(**model_kwargs)
self._load(**model_kwargs)
self._loaded = True
def load(self) -> None:
if self.loaded:
return
self.download()
log.info(f"Loading {self.model_type.replace('-', ' ')} model '{self.model_name}'")
self._load()
self.loaded = True
def predict(self, inputs: Any, **model_kwargs: Any) -> Any:
if not self._loaded:
log.info(f"Loading {self.model_type.replace('_', ' ')} model '{self.model_name}'")
self.load()
self.load()
if model_kwargs:
self.configure(**model_kwargs)
return self._predict(inputs)
@@ -95,11 +79,11 @@ class InferenceModel(ABC):
pass
@abstractmethod
def _download(self, **model_kwargs: Any) -> None:
def _download(self) -> None:
...
@abstractmethod
def _load(self, **model_kwargs: Any) -> None:
def _load(self) -> None:
...
@property

View File

@@ -17,7 +17,7 @@ class ModelCache:
revalidate: bool = False,
timeout: int | None = None,
profiling: bool = False,
):
) -> None:
"""
Args:
ttl: Unloads model after this duration. Disabled if None. Defaults to None.

View File

@@ -42,7 +42,7 @@ class CLIPEncoder(InferenceModel):
jina_model_name = self._get_jina_model_name(model_name)
super().__init__(jina_model_name, cache_dir, **model_kwargs)
def _download(self, **model_kwargs: Any) -> None:
def _download(self) -> None:
models: tuple[tuple[str, str], tuple[str, str]] = _MODELS[self.model_name]
text_onnx_path = self.cache_dir / "textual.onnx"
vision_onnx_path = self.cache_dir / "visual.onnx"
@@ -53,8 +53,9 @@ class CLIPEncoder(InferenceModel):
if not vision_onnx_path.is_file():
self._download_model(*models[1])
def _load(self, **model_kwargs: Any) -> None:
def _load(self) -> None:
if self.mode == "text" or self.mode is None:
log.debug(f"Loading clip text model '{self.model_name}'")
self.text_model = ort.InferenceSession(
self.cache_dir / "textual.onnx",
sess_options=self.sess_options,
@@ -65,6 +66,7 @@ class CLIPEncoder(InferenceModel):
self.tokenizer = Tokenizer(self.model_name)
if self.mode == "vision" or self.mode is None:
log.debug(f"Loading clip vision model '{self.model_name}'")
self.vision_model = ort.InferenceSession(
self.cache_dir / "visual.onnx",
sess_options=self.sess_options,

View File

@@ -26,7 +26,7 @@ class FaceRecognizer(InferenceModel):
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self, **model_kwargs: Any) -> None:
def _download(self) -> None:
zip_file = self.cache_dir / f"{self.model_name}.zip"
download_file(f"{BASE_REPO_URL}/{self.model_name}.zip", zip_file)
with zipfile.ZipFile(zip_file, "r") as zip:
@@ -36,7 +36,7 @@ class FaceRecognizer(InferenceModel):
zip.extractall(self.cache_dir, members=[det_file, rec_file])
zip_file.unlink()
def _load(self, **model_kwargs: Any) -> None:
def _load(self) -> None:
try:
det_file = next(self.cache_dir.glob("det_*.onnx"))
rec_file = next(self.cache_dir.glob("w600k_*.onnx"))

View File

@@ -26,7 +26,7 @@ class ImageClassifier(InferenceModel):
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self, **model_kwargs: Any) -> None:
def _download(self) -> None:
snapshot_download(
cache_dir=self.cache_dir,
repo_id=self.model_name,
@@ -35,10 +35,10 @@ class ImageClassifier(InferenceModel):
local_dir_use_symlinks=True,
)
def _load(self, **model_kwargs: Any) -> None:
def _load(self) -> None:
processor = AutoImageProcessor.from_pretrained(self.cache_dir, cache_dir=self.cache_dir)
model_path = self.cache_dir / "model.onnx"
model_kwargs |= {
model_kwargs = {
"cache_dir": self.cache_dir,
"provider": self.providers[0],
"provider_options": self.provider_options[0],

View File

@@ -1,11 +1,11 @@
import json
import pickle
from io import BytesIO
from typing import TypeAlias
from typing import Any, TypeAlias
from unittest import mock
import cv2
import numpy as np
import onnxruntime as ort
import pytest
from fastapi.testclient import TestClient
from PIL import Image
@@ -31,23 +31,6 @@ class TestImageClassifier:
{"label": "probably a virus", "score": 0.01},
]
def test_eager_init(self, mocker: MockerFixture) -> None:
mocker.patch.object(ImageClassifier, "download")
mock_load = mocker.patch.object(ImageClassifier, "load")
classifier = ImageClassifier("test_model_name", cache_dir="test_cache", eager=True, test_arg="test_arg")
assert classifier.model_name == "test_model_name"
mock_load.assert_called_once_with(test_arg="test_arg")
def test_lazy_init(self, mocker: MockerFixture) -> None:
mock_download = mocker.patch.object(ImageClassifier, "download")
mock_load = mocker.patch.object(ImageClassifier, "load")
face_model = ImageClassifier("test_model_name", cache_dir="test_cache", eager=False, test_arg="test_arg")
assert face_model.model_name == "test_model_name"
mock_download.assert_called_once_with(test_arg="test_arg")
mock_load.assert_not_called()
def test_min_score(self, pil_image: Image.Image, mocker: MockerFixture) -> None:
mocker.patch.object(ImageClassifier, "load")
classifier = ImageClassifier("test_model_name", min_score=0.0)
@@ -74,23 +57,6 @@ class TestImageClassifier:
class TestCLIP:
embedding = np.random.rand(512).astype(np.float32)
def test_eager_init(self, mocker: MockerFixture) -> None:
mocker.patch.object(CLIPEncoder, "download")
mock_load = mocker.patch.object(CLIPEncoder, "load")
clip_model = CLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", eager=True, test_arg="test_arg")
assert clip_model.model_name == "ViT-B-32::openai"
mock_load.assert_called_once_with(test_arg="test_arg")
def test_lazy_init(self, mocker: MockerFixture) -> None:
mock_download = mocker.patch.object(CLIPEncoder, "download")
mock_load = mocker.patch.object(CLIPEncoder, "load")
clip_model = CLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", eager=False, test_arg="test_arg")
assert clip_model.model_name == "ViT-B-32::openai"
mock_download.assert_called_once_with(test_arg="test_arg")
mock_load.assert_not_called()
def test_basic_image(self, pil_image: Image.Image, mocker: MockerFixture) -> None:
mocker.patch.object(CLIPEncoder, "download")
mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
@@ -119,23 +85,6 @@ class TestCLIP:
class TestFaceRecognition:
def test_eager_init(self, mocker: MockerFixture) -> None:
mocker.patch.object(FaceRecognizer, "download")
mock_load = mocker.patch.object(FaceRecognizer, "load")
face_model = FaceRecognizer("test_model_name", cache_dir="test_cache", eager=True, test_arg="test_arg")
assert face_model.model_name == "test_model_name"
mock_load.assert_called_once_with(test_arg="test_arg")
def test_lazy_init(self, mocker: MockerFixture) -> None:
mock_download = mocker.patch.object(FaceRecognizer, "download")
mock_load = mocker.patch.object(FaceRecognizer, "load")
face_model = FaceRecognizer("test_model_name", cache_dir="test_cache", eager=False, test_arg="test_arg")
assert face_model.model_name == "test_model_name"
mock_download.assert_called_once_with(test_arg="test_arg")
mock_load.assert_not_called()
def test_set_min_score(self, mocker: MockerFixture) -> None:
mocker.patch.object(FaceRecognizer, "load")
face_recognizer = FaceRecognizer("test_model_name", cache_dir="test_cache", min_score=0.5)
@@ -220,45 +169,64 @@ class TestCache:
reason="More time-consuming since it deploys the app and loads models.",
)
class TestEndpoints:
def test_tagging_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None:
def test_tagging_endpoint(
self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
) -> None:
byte_image = BytesIO()
pil_image.save(byte_image, format="jpeg")
headers = {"Content-Type": "image/jpg"}
response = deployed_app.post(
"http://localhost:3003/image-classifier/tag-image",
content=byte_image.getvalue(),
headers=headers,
"http://localhost:3003/predict",
data={
"modelName": "microsoft/resnet-50",
"modelType": "image-classification",
"options": json.dumps({"minScore": 0.0}),
},
files={"image": byte_image.getvalue()},
)
assert response.status_code == 200
assert response.json() == responses["image-classification"]
def test_clip_image_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None:
def test_clip_image_endpoint(
self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
) -> None:
byte_image = BytesIO()
pil_image.save(byte_image, format="jpeg")
headers = {"Content-Type": "image/jpg"}
response = deployed_app.post(
"http://localhost:3003/sentence-transformer/encode-image",
content=byte_image.getvalue(),
headers=headers,
"http://localhost:3003/predict",
data={"modelName": "ViT-B-32::openai", "modelType": "clip", "options": json.dumps({"mode": "vision"})},
files={"image": byte_image.getvalue()},
)
assert response.status_code == 200
assert response.json() == responses["clip"]["image"]
def test_clip_text_endpoint(self, deployed_app: TestClient) -> None:
def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
response = deployed_app.post(
"http://localhost:3003/sentence-transformer/encode-text",
json={"text": "test search query"},
"http://localhost:3003/predict",
data={
"modelName": "ViT-B-32::openai",
"modelType": "clip",
"text": "test search query",
"options": json.dumps({"mode": "text"}),
},
)
assert response.status_code == 200
assert response.json() == responses["clip"]["text"]
def test_face_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None:
def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
byte_image = BytesIO()
pil_image.save(byte_image, format="jpeg")
headers = {"Content-Type": "image/jpg"}
response = deployed_app.post(
"http://localhost:3003/facial-recognition/detect-faces",
content=byte_image.getvalue(),
headers=headers,
"http://localhost:3003/predict",
data={
"modelName": "buffalo_l",
"modelType": "facial-recognition",
"options": json.dumps({"minScore": 0.034}),
},
files={"image": byte_image.getvalue()},
)
assert response.status_code == 200
assert response.json() == responses["facial-recognition"]
def test_sess_options() -> None:

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.77.0"
version = "1.78.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,11 @@ export LD_PRELOAD="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
: "${MACHINE_LEARNING_HOST:=0.0.0.0}"
: "${MACHINE_LEARNING_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}"
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \
-w $MACHINE_LEARNING_WORKERS \
-b $MACHINE_LEARNING_HOST:$MACHINE_LEARNING_PORT \
-t $MACHINE_LEARNING_WORKER_TIMEOUT \
--log-config-json log_conf.json

11
mobile/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"dart.flutterSdkPath": ".fvm/flutter_sdk",
// Remove .fvm files from search
"search.exclude": {
"**/.fvm": true
},
// Remove from file watching
"files.watcherExclude": {
"**/.fvm": true
}
}

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 100,
"android.injected.version.name" => "1.77.0",
"android.injected.version.code" => 101,
"android.injected.version.name" => "1.78.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -1,11 +1,11 @@
{
"add_to_album_bottom_sheet_added": "Přidáno do {album}",
"add_to_album_bottom_sheet_already_exists": "Již v {album}",
"add_to_album_bottom_sheet_already_exists": "Je již v {album}",
"advanced_settings_prefer_remote_subtitle": "U některých zařízení je načítání miniatur z prostředků v zařízení velmi pomalé. Aktivujte toto nastavení, aby se místo toho načítaly vzdálené obrázky.",
"advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky",
"advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení",
"advanced_settings_tile_title": "Pokročilé",
"advanced_settings_troubleshooting_subtitle": "Povolit dodatečné funkce pro řešení problémů",
"advanced_settings_troubleshooting_subtitle": "Zobrazit dodatečné vlastnosti pro řešení problémů",
"advanced_settings_troubleshooting_title": "Řešení problémů",
"album_info_card_backup_album_excluded": "VYLOUČENO",
"album_info_card_backup_album_included": "ZAHRNUTO",
@@ -16,8 +16,8 @@
"album_thumbnail_shared_by": "Sdílené od {}",
"album_viewer_appbar_share_delete": "Odstranit album",
"album_viewer_appbar_share_err_delete": "Nepodařilo se odstranit album",
"album_viewer_appbar_share_err_leave": "Nepodařilo se ukončit album",
"album_viewer_appbar_share_err_remove": "Při odstraňování souborů z alba se vyskytly problémy.",
"album_viewer_appbar_share_err_leave": "Nepodařilo se opustit album",
"album_viewer_appbar_share_err_remove": "Při odstraňování položek z alba se vyskytly problémy.",
"album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba",
"album_viewer_appbar_share_leave": "Opustit album",
"album_viewer_appbar_share_remove": "Odstranit z alba",
@@ -35,18 +35,18 @@
"asset_list_settings_title": "Fotografická mřížka",
"backup_album_selection_page_albums_device": "Alba v zařízení ({})",
"backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, dvojím klepnutím ji vyloučíte",
"backup_album_selection_page_assets_scatter": "Soubory mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.",
"backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.",
"backup_album_selection_page_select_albums": "Vybraná alba",
"backup_album_selection_page_selection_info": "Informace o výběru",
"backup_album_selection_page_total_assets": "Celkový počet jedinečných souborů",
"backup_album_selection_page_total_assets": "Celkový počet jedinečných položek",
"backup_all": "Vše",
"backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu...",
"backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu...",
"backup_background_service_current_upload_notification": "Nahrávání {}",
"backup_background_service_default_notification": "Kontrola nových médií {}",
"backup_background_service_current_upload_notification": "Zálohování {}",
"backup_background_service_default_notification": "Kontrola nových médií",
"backup_background_service_error_title": "Chyba zálohování",
"backup_background_service_in_progress_notification": "Vytvářím kopii vašich médií...",
"backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {}",
"backup_background_service_in_progress_notification": "Zálohování vašich médií...",
"backup_background_service_upload_failure_notification": "Nepodařilo se zálohovat {}",
"backup_controller_page_albums": "Zálohovaná alba",
"backup_controller_page_background_app_refresh_disabled_content": "Povolte obnovení aplikace na pozadí v Nastavení > Obecné > Obnovení aplikace na pozadí, abyste mohli používat zálohování na pozadí.",
"backup_controller_page_background_app_refresh_disabled_title": " Obnovování aplikací na pozadí je vypnuté",
@@ -58,12 +58,12 @@
"backup_controller_page_background_charging": "Pouze během nabíjení",
"backup_controller_page_background_configure_error": "Nepodařilo se nakonfigurovat službu na pozadí",
"backup_controller_page_background_delay": "Zpoždění zálohování nových médií: {}",
"backup_controller_page_background_description": "Povolte službu na pozadí pro automatické zálohování všech nových aktiv bez nutnosti otevření aplikace",
"backup_controller_page_background_description": "Povolte službu na pozadí pro automatické zálohování všech nových položek bez nutnosti otevření aplikace",
"backup_controller_page_background_is_off": "Automatické zálohování na pozadí je vypnuto",
"backup_controller_page_background_is_on": "Automatické zálohování na pozadí je zapnuto",
"backup_controller_page_background_turn_off": "Vypnout zálohování na pozadí",
"backup_controller_page_background_turn_on": "Povolit zálohování na pozadí",
"backup_controller_page_background_wifi": "Jen na WiFi",
"backup_controller_page_background_wifi": "Jen na Wi-Fi",
"backup_controller_page_backup": "Zálohování",
"backup_controller_page_backup_selected": "Vybrané: ",
"backup_controller_page_backup_sub": "Zálohované fotografie a videa",
@@ -76,7 +76,7 @@
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Informace o zálohování",
"backup_controller_page_none_selected": "Žádné vybrané",
"backup_controller_page_remainder": "Zůstává",
"backup_controller_page_remainder": "Zvá",
"backup_controller_page_remainder_sub": "Zbývající fotografie a videa, která se mají zálohovat z vybraných alb",
"backup_controller_page_select": "Vybrat",
"backup_controller_page_server_storage": "Serverové úložiště",
@@ -89,12 +89,12 @@
"backup_controller_page_total_sub": "Všechny jedinečné fotografie a videa z vybraných alb",
"backup_controller_page_turn_off": "Vypnout zálohování na popředí",
"backup_controller_page_turn_on": "Povolit zálohování na popředí",
"backup_controller_page_uploading_file_info": "Nahrávaný soubor",
"backup_controller_page_uploading_file_info": "Informace o zálohovaném souboru",
"backup_err_only_album": "Nelze odstranit jediné vybrané album",
"backup_info_card_assets": "položek",
"backup_manual_cancelled": "Cancelled",
"backup_manual_cancelled": "Zrušeno",
"backup_manual_failed": "Selhalo",
"backup_manual_in_progress": "Zálohování již probíhá. Zkuste znovu později",
"backup_manual_in_progress": "Zálohování již probíhá. Zkuste to znovu později",
"backup_manual_success": "Úspěch",
"backup_manual_title": "Stav zálohování",
"cache_settings_album_thumbnails": "Náhledy stránek knihovny (položek {})",
@@ -111,7 +111,7 @@
"cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů (položek {})",
"cache_settings_title": "Nastavení vyrovnávací paměti",
"change_password_form_confirm_password": "Potvrďte heslo",
"change_password_form_description": "Dobrý den, {firstName} {lastName},\n\nJe to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Zadejte níže, prosím, nové heslo.",
"change_password_form_description": "Dobrý den, {firstName} {lastName},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.",
"change_password_form_new_password": "Nové heslo",
"change_password_form_password_mismatch": "Hesla se neshodují",
"change_password_form_reenter_new_password": "Znovu zadejte nové heslo",
@@ -122,7 +122,7 @@
"common_shared": "Sdílené",
"control_bottom_app_bar_add_to_album": "Přidat do alba",
"control_bottom_app_bar_album_info": "{} položek",
"control_bottom_app_bar_album_info_shared": "{} položky - sdílené",
"control_bottom_app_bar_album_info_shared": "{} položky sdílené",
"control_bottom_app_bar_archive": "Archív",
"control_bottom_app_bar_create_new_album": "Vytvořit nové album",
"control_bottom_app_bar_delete": "Vymazat",
@@ -132,14 +132,14 @@
"create_album_page_untitled": "Bez názvu",
"create_shared_album_page_create": "Vytvořit",
"create_shared_album_page_share": "Sdílet",
"create_shared_album_page_share_add_assets": "PŘIDAT",
"create_shared_album_page_share_add_assets": "PŘIDAT POLOŽKY",
"create_shared_album_page_share_select_photos": "Vybrat fotografie",
"curated_location_page_title": "Místa",
"curated_object_page_title": "Věci",
"daily_title_text_date": "EEEE, d. MMMM",
"daily_title_text_date_year": "EEEE, d. MMMM y",
"date_format": "EEEE, d. MMMM y • H:mm",
"delete_dialog_alert": "Tyto položky budou trvale odstraněny z Immich a z vašeho zařízení.",
"delete_dialog_alert": "Tyto položky budou trvale odstraněny z Immich i z vašeho zařízení",
"delete_dialog_cancel": "Zrušit",
"delete_dialog_ok": "Vymazat",
"delete_dialog_title": "Vymazat trvale",
@@ -155,13 +155,13 @@
"favorites_page_no_favorites": "Nebyla nalezena žádná oblíbená média",
"favorites_page_title": "Oblíbené",
"home_page_add_to_album_conflicts": "Přidáno {added} položek do alba {album}. {failed} položek již je v albu.",
"home_page_add_to_album_err_local": "Zatím není možné přidat lokální média do alb, přeskakuje se",
"home_page_add_to_album_err_local": "Zatím není možné přidat lokální média do alb, přeskakuji",
"home_page_add_to_album_success": "Přidány položky {added} do alba {album}.",
"home_page_archive_err_local": "Zatím nemohu archivovat lokální média, přeskakuji",
"home_page_building_timeline": "Vytváření časové osy",
"home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuje se",
"home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných albech.",
"home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji",
"home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji",
"home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb.",
"home_page_upload_err_limit": "Lze zálohovat nejvýše 30 položek najednou, přeskakuji",
"image_viewer_page_state_provider_download_error": "Chyba stahování",
"image_viewer_page_state_provider_download_success": "Stahování bylo úspěšné",
"library_page_albums": "Alba",
@@ -175,7 +175,7 @@
"login_disabled": "Přihlášení bylo zakázáno",
"login_form_api_exception": "Výjimka API. Zkontrolujte URL serveru a zkuste to znovu.",
"login_form_button_text": "Přihlásit se",
"login_form_email_hint": "tvůjmail@email.com",
"login_form_email_hint": "tvůje-mail@email.com",
"login_form_endpoint_hint": "http://ip-tvého-serveru:port/api",
"login_form_endpoint_url": "URL adresa serveru",
"login_form_err_http": "Prosím, uveďte http:// nebo https://",
@@ -185,7 +185,7 @@
"login_form_err_trailing_whitespace": "Koncová mezera",
"login_form_failed_get_oauth_server_config": "Chyba přihlášení pomocí OAuth, zkontrolujte adresu URL serveru",
"login_form_failed_get_oauth_server_disable": "Funkce OAuth není na tomto serveru dostupná",
"login_form_failed_login": "Chyba přihlášení, zkontrolujte url adresu serveru, e-mail a heslo.",
"login_form_failed_login": "Chyba přihlášení, zkontrolujte URL adresu serveru, e-mail a heslo.",
"login_form_label_email": "E-mail",
"login_form_label_password": "Heslo",
"login_form_next_button": "Další",
@@ -196,7 +196,7 @@
"monthly_title_text_date_format": "LLLL y",
"motion_photos_page_title": "Pohyblivé fotky",
"notification_permission_dialog_cancel": "Zrušit",
"notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do Nastavení a vyberte možnost povolit.",
"notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do nastavení a vyberte možnost povolit.",
"notification_permission_dialog_settings": "Nastavení",
"notification_permission_list_tile_content": "Udělte oprávnění k aktivaci oznámení.",
"notification_permission_list_tile_enable_button": "Povolit oznámení",
@@ -215,16 +215,16 @@
"permission_onboarding_go_to_settings": "Přejít do nastavení",
"permission_onboarding_grant_permission": "Povolit přístup",
"permission_onboarding_log_out": "Odhlásit se",
"permission_onboarding_permission_denied": "Přístup odepřen. Pro používání Immich, je nutné povolit přístup k fotkám a videím v nastavení.",
"permission_onboarding_permission_denied": "Přístup odepřen. Pro používání Immich je nutné povolit přístup k fotkám a videím v nastavení.",
"permission_onboarding_permission_granted": "Přístup povolen! Vše je připraveno.",
"permission_onboarding_permission_limited": "Přístup omezen. Chcete-li používat Immich k zálohování a správě celé vaší kolekci galerií, povolte přístup k fotkám a videím v Nastavení.",
"permission_onboarding_permission_limited": "Přístup omezen. Chcete-li používat Immich k zálohování a správě celé vaší kolekce galerií, povolte v nastavení přístup k fotkám a videím.",
"permission_onboarding_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.",
"profile_drawer_app_logs": "Logy",
"profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální",
"profile_drawer_settings": "Nastavení",
"profile_drawer_sign_out": "Odhlásit se",
"recently_added_page_title": "Nedávno přidané",
"search_bar_hint": "Prohledejte své obrázky",
"search_bar_hint": "Prohledejte své fotky",
"search_page_categories": "Kategorie",
"search_page_favorites": "Oblíbené",
"search_page_motion_photos": "Pohyblivé fotky",
@@ -234,7 +234,7 @@
"search_page_places": "Místa",
"search_page_recently_added": "Nedávno přidané",
"search_page_screenshots": "Snímky obrazovky",
"search_page_selfies": "Selfie",
"search_page_selfies": "Autoportréty",
"search_page_things": "Věci",
"search_page_videos": "Videa",
"search_page_view_all_button": "Zobrazit vše",
@@ -258,11 +258,11 @@
"setting_notifications_notify_minutes": "{} minut",
"setting_notifications_notify_never": "nikdy",
"setting_notifications_notify_seconds": "{} sekundy",
"setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu nahrávání pro položku",
"setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu zálohování položky",
"setting_notifications_single_progress_title": "Zobrazit průběh detailů zálohování na pozadí",
"setting_notifications_subtitle": "Přizpůsobení předvoleb oznámení",
"setting_notifications_title": "Oznámení",
"setting_notifications_total_progress_subtitle": "Celkový průběh nahrávání (nahraných/celkově)",
"setting_notifications_total_progress_subtitle": "Celkový průběh zálohování (hotovo/celkově)",
"setting_notifications_total_progress_title": "Zobrazit celkový průběh zálohování na pozadí",
"setting_pages_app_bar_settings": "Nastavení",
"settings_require_restart": "Pro použití tohoto nastavení restartujte Immich",
@@ -297,7 +297,7 @@
"upload_dialog_title": "Zálohovat položku",
"version_announcement_overlay_ack": "Potvrdit",
"version_announcement_overlay_release_notes": "poznámky k vydání",
"version_announcement_overlay_text_1": "Ahoj, je zde nová verze",
"version_announcement_overlay_text_1": "Ahoj, k dispozici je nová verze",
"version_announcement_overlay_text_2": "najděte si čas na návštěvu ",
"version_announcement_overlay_text_3": " a ujistěte se, že vaše konfigurace docker-compose a .env je aktuální, abyste předešli nesprávné konfiguraci, zvláště pokud používáte WatchTower nebo jakýkoli mechanismus, který podporuje automatické aktualizace serverových aplikací.",
"version_announcement_overlay_title": "K dispozici je nová verze serveru \uD83C\uDF89"

View File

@@ -40,29 +40,29 @@
"backup_album_selection_page_selection_info": "Oplysninger om valgte",
"backup_album_selection_page_total_assets": "Samlede unikke elementer",
"backup_all": "Alt",
"backup_background_service_backup_failed_message": "Backup af elementer fejlede. Forsøger igen...",
"backup_background_service_backup_failed_message": "Sikkerhedskopiering af elementer fejlede. Forsøger igen...",
"backup_background_service_connection_failed_message": "Forbindelsen til serveren blev tabt. Forsøger igen...",
"backup_background_service_current_upload_notification": "Uploader {}",
"backup_background_service_default_notification": "Søger efter nye elementer...",
"backup_background_service_error_title": "Fejl med backup",
"backup_background_service_in_progress_notification": "Tager backup af dine elementer...",
"backup_background_service_error_title": "Fejl med sikkerhedskopiering",
"backup_background_service_in_progress_notification": "Tager sikkerhedskopi af dine elementer...",
"backup_background_service_upload_failure_notification": "Fejlede med uploade af {}",
"backup_controller_page_albums": "Sikkerhedskopiér albummer",
"backup_controller_page_background_app_refresh_disabled_content": "Slå baggrundsopdatering af applikationen til i Indstillinger > Generelt > Baggrundsopdatering af applikationer, for at bruge baggrundsbackup.",
"backup_controller_page_background_app_refresh_disabled_content": "Slå baggrundsopdatering af applikationen til i Indstillinger > Generelt > Baggrundsopdatering af applikationer, for at bruge sikkerhedskopi i baggrunden.",
"backup_controller_page_background_app_refresh_disabled_title": "Baggrundsopdatering af app er slået fra",
"backup_controller_page_background_app_refresh_enable_button_text": "Gå til indstillinger",
"backup_controller_page_background_battery_info_link": "Vis mig hvordan",
"backup_controller_page_background_battery_info_message": "For den bedste oplevelse med baggrundsbackup, bør du slå batterioptimering, der begrænder baggrundsaktivitet, fra.\n\nSiden dette er afhængigt af enheden, bør du undersøge denne information leveret af din enheds producent.",
"backup_controller_page_background_battery_info_message": "For den bedste oplevelse med sikkerhedskopiering i baggrunden, bør du slå batterioptimering, der begrænder baggrundsaktivitet, fra.\n\nSiden dette er afhængigt af enheden, bør du undersøge denne information leveret af din enheds producent.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Batterioptimering",
"backup_controller_page_background_charging": "Kun under opladning",
"backup_controller_page_background_configure_error": "Fejlede konfigureringen af baggrundsbackup",
"backup_controller_page_background_delay": "Udskyd backup af nye elementer: {}",
"backup_controller_page_background_description": "Slå baggrundsbackup til, for automatisk at tage backup af nye elementer, uden at skulle åbne appen",
"backup_controller_page_background_is_off": "Automatisk baggrundsbackup er slået fra",
"backup_controller_page_background_is_on": "Automatisk baggrundsbackup er slået til",
"backup_controller_page_background_turn_off": "Slå baggrundsbackup fra",
"backup_controller_page_background_turn_on": "Slå baggrundsbackup til",
"backup_controller_page_background_configure_error": "Fejlede konfigureringen af sikkerhedskopiering i baggrunden",
"backup_controller_page_background_delay": "Udskyd sikkerhedskopi af nye elementer: {}",
"backup_controller_page_background_description": "Slå sikkerhedskopiering i baggrunden til, for automatisk at tage sikkerhedskopi af nye elementer, uden at skulle åbne appen",
"backup_controller_page_background_is_off": "Automatisk sikkerhedskopiering i baggrunden er slået fra",
"backup_controller_page_background_is_on": "Automatisk sikkerhedskopiering i baggrunden er slået til",
"backup_controller_page_background_turn_off": "Slå sikkerhedskopiering i baggrunden fra",
"backup_controller_page_background_turn_on": "Slå sikkerhedskopiering i baggrunden til",
"backup_controller_page_background_wifi": "Kun med WiFi",
"backup_controller_page_backup": "Sikkerhedskopier",
"backup_controller_page_backup_selected": "Valgte: ",
@@ -92,11 +92,11 @@
"backup_controller_page_uploading_file_info": "Uploader filinformation",
"backup_err_only_album": "Kan ikke slette det eneste album",
"backup_info_card_assets": "elementer",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "Annulleret",
"backup_manual_failed": "Mislykkedes",
"backup_manual_in_progress": "Upload er allerede undervejs. Prøv igen efter noget tid",
"backup_manual_success": "Succes",
"backup_manual_title": "Uploadstatus",
"cache_settings_album_thumbnails": "Biblioteksminiaturebilleder ({} elementer)",
"cache_settings_clear_cache_button": "Fjern cache",
"cache_settings_clear_cache_button_title": "Fjern appens cache. Dette vil i stor grad påvirke appens ydeevne indtil cachen er genopbygget.",
@@ -160,8 +160,8 @@
"home_page_archive_err_local": "Kan ikke arkivere lokalt element endnu.. Springer over",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..",
"home_page_first_time_notice": "Hvis dette er din første gang i appen, bedes du vælge en backup af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vælge en sikkerhedskopi af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
"home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over",
"image_viewer_page_state_provider_download_error": "Fejl ved download",
"image_viewer_page_state_provider_download_success": "Download succesfuld",
"library_page_albums": "Albummer",
@@ -172,7 +172,7 @@
"library_page_sharing": "Delte",
"library_page_sort_created": "Senest oprettet",
"library_page_sort_title": "Albumtitel",
"login_disabled": "Login has been disabled",
"login_disabled": "Login er blevet deaktiveret",
"login_form_api_exception": "API-undtagelse. Tjek serverens URL og prøv igen. ",
"login_form_button_text": "Log ind",
"login_form_email_hint": "din-e-mail@e-mail.com",
@@ -217,7 +217,7 @@
"permission_onboarding_log_out": "Log ud",
"permission_onboarding_permission_denied": "Tilladelse afvist. For at bruge Immich, skal der gives tilladelse til at se billeder og videoer i indstillinger.",
"permission_onboarding_permission_granted": "Tilladelse givet! Du er nu klar.",
"permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave backup og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.",
"permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.",
"permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.",
"profile_drawer_app_logs": "Log",
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
@@ -252,7 +252,7 @@
"setting_image_viewer_original_title": "Indlæs originalbillede",
"setting_image_viewer_preview_subtitle": "Slå indlæsning af et mediumstørrelse billede til. Slå fra for enten direkte at indlæse originalen eller kun at bruge miniaturebilledet.",
"setting_image_viewer_preview_title": "Indlæs forhåndsvisning af billedet",
"setting_notifications_notify_failures_grace_period": "Giv besked om baggrundsbackupfejl: {}",
"setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {}",
"setting_notifications_notify_hours": "{} timer",
"setting_notifications_notify_immediately": "med det samme",
"setting_notifications_notify_minutes": "{} minutter",
@@ -291,10 +291,10 @@
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Tre-trins indlæsning kan øge ydeevnen, men kan ligeledes føre til højere netværksbelastning",
"theme_setting_three_stage_loading_title": "Slå tre-trins indlæsning til",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_cancel": "Annuller",
"upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_title": "Upload element",
"version_announcement_overlay_ack": "Accepter",
"version_announcement_overlay_release_notes": "udgivelsesnoterne",
"version_announcement_overlay_text_1": "Hej ven, der er en ny version af",

View File

@@ -7,6 +7,8 @@
"advanced_settings_tile_title": "Advanced",
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
"advanced_settings_troubleshooting_title": "Troubleshooting",
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_thumbnail_card_item": "1 item",
@@ -174,6 +176,7 @@
"library_page_sort_title": "Album title",
"login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",

View File

@@ -1,8 +1,8 @@
{
"add_to_album_bottom_sheet_added": "Lisätty albumiin {album}",
"add_to_album_bottom_sheet_already_exists": "Kohde on jo albumissa {album}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_prefer_remote_subtitle": "Jotkut laitteet ovat erittäin hitaita lataamaan esikatselukuvia laitteen kohteista. Aktivoi tämä asetus käyttääksesi etäkuvia.",
"advanced_settings_prefer_remote_title": "Suosi etäkuvia",
"advanced_settings_tile_subtitle": "Edistyneen käyttäjän asetukset",
"advanced_settings_tile_title": "Edistyneet",
"advanced_settings_troubleshooting_subtitle": "Kytke vianetsinnän lisäominaisuudet päälle",
@@ -22,12 +22,12 @@
"album_viewer_appbar_share_leave": "Poistu albumista",
"album_viewer_appbar_share_remove": "Poista albumista",
"album_viewer_page_share_add_users": "Lisää käyttäjiä",
"all_people_page_title": "People",
"all_people_page_title": "Ihmiset",
"all_videos_page_title": "Videot",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_no_archived_assets": "Arkistoituja kohteita ei löytynyt",
"archive_page_title": "Arkisto ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynaaminen asetelma",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_automatically": "Automaattisesti",
"asset_list_layout_settings_group_by": "Ryhmittele",
"asset_list_layout_settings_group_by_month": "Kuukauden mukaan",
"asset_list_layout_settings_group_by_month_day": "Kuukauden ja päivän mukaan",
@@ -69,7 +69,7 @@
"backup_controller_page_backup_sub": "Varmuuskopioidut kuvat ja videot",
"backup_controller_page_cancel": "Peruuta",
"backup_controller_page_created": "Luotu: {}",
"backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle ladataksesi uudet kohteet palvelimelle automaattisesti.",
"backup_controller_page_desc_backup": "Kytke varmuuskopiointi päälle lähettääksesi uudet kohteet palvelimelle automaattisesti.",
"backup_controller_page_excluded": "Jätetty pois:",
"backup_controller_page_failed": "Epäonnistui ({})",
"backup_controller_page_filename": "Tiedoston nimi: {} [{}]",
@@ -92,11 +92,11 @@
"backup_controller_page_uploading_file_info": "Tiedostojen lähetystiedot",
"backup_err_only_album": "Vähintään yhden albumin tulee olla valittuna",
"backup_info_card_assets": "kohdetta",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "Peruutettu",
"backup_manual_failed": "Epäonnistui",
"backup_manual_in_progress": "Lähetys palvelimelle on jo käynnissä. Kokeile uudelleen hetken kuluttua.",
"backup_manual_success": "Onnistui",
"backup_manual_title": "Lähetyksen tila",
"cache_settings_album_thumbnails": "Kirjastosivun esikatselukuvat ({} kohdetta)",
"cache_settings_clear_cache_button": "Tyhjennä välimuisti",
"cache_settings_clear_cache_button_title": "Tyhjennä sovelluksen välimuisti. Tämä vaikuttaa merkittävästi sovelluksen suorituskykyyn, kunnes välimuisti on rakennettu uudelleen.",
@@ -128,7 +128,7 @@
"control_bottom_app_bar_delete": "Poista",
"control_bottom_app_bar_favorite": "Suosikki",
"control_bottom_app_bar_share": "Jaa",
"control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_unarchive": "Palauta arkistosta",
"create_album_page_untitled": "Nimetön",
"create_shared_album_page_create": "Luo",
"create_shared_album_page_share": "Jaa",
@@ -152,7 +152,7 @@
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
"experimental_settings_title": "Kokeellinen",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_no_favorites": "Suosikkikohteita ei löytynyt",
"favorites_page_title": "Suosikit",
"home_page_add_to_album_conflicts": "Lisätty {added} kohdetta albumiin {album}. {failed} kohdetta on jo albumissa.",
"home_page_add_to_album_err_local": "Paikallisten kohteiden lisääminen albumeihin ei ole mahdollista, ohitetaan",
@@ -161,7 +161,7 @@
"home_page_building_timeline": "Rakennetaan aikajanaa",
"home_page_favorite_err_local": "Paikallisten kohteiden lisääminen suosikkeihin ei ole mahdollista, ohitetaan",
"home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan",
"image_viewer_page_state_provider_download_error": "Lataus epäonnistui",
"image_viewer_page_state_provider_download_success": "Lataus onnistui",
"library_page_albums": "Albumit",
@@ -172,7 +172,7 @@
"library_page_sharing": "Jakaminen",
"library_page_sort_created": "Viimeisin luotu",
"library_page_sort_title": "Albumin otsikko",
"login_disabled": "Login has been disabled",
"login_disabled": "Kirjautuminen on poistettu käytöstä",
"login_form_api_exception": "API-virhe. Tarkista palvelimen URL-osoite ja yritä uudelleen.",
"login_form_button_text": "Kirjaudu",
"login_form_email_hint": "sahkopostisi@esimerkki.fi",
@@ -201,15 +201,15 @@
"notification_permission_list_tile_content": "Myönnä käyttöoikeus ottaaksesi ilmoitukset käyttöön.",
"notification_permission_list_tile_enable_button": "Ota ilmoitukset käyttöön",
"notification_permission_list_tile_title": "Ilmoitusten käyttöoikeus",
"partner_page_add_partner": "Add partner",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_select_partner": "Select partner",
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_title": "Partner",
"partner_page_add_partner": "Lisää kumppani",
"partner_page_empty_message": "Kuviasi ei ole vielä jaettu kenenkään kumppanin kanssa.",
"partner_page_no_more_users": "Ei enempää käyttäjiä lisättäväksi",
"partner_page_partner_add_failed": "Kumppanin lisääminen epäonnistui",
"partner_page_select_partner": "Valitse kumppani",
"partner_page_shared_to_title": "Jaettu henkilöille",
"partner_page_stop_sharing_content": "{} ei voi enää käyttää kuviasi.",
"partner_page_stop_sharing_title": "Lopetetaanko kuvien jakaminen?",
"partner_page_title": "Kumppani",
"permission_onboarding_continue_anyway": "Jatka silti",
"permission_onboarding_get_started": "Aloittaminen",
"permission_onboarding_go_to_settings": "Siirry asetuksiin",
@@ -230,7 +230,7 @@
"search_page_motion_photos": "Liikekuvat",
"search_page_no_objects": "Objektitietoja ei ole saatavilla",
"search_page_no_places": "Paikkatietoja ei ole saatavilla",
"search_page_people": "People",
"search_page_people": "Ihmiset",
"search_page_places": "Paikat",
"search_page_recently_added": "Viimeksi lisätyt",
"search_page_screenshots": "Näyttökuvat",
@@ -291,10 +291,10 @@
"theme_setting_theme_title": "Teema",
"theme_setting_three_stage_loading_subtitle": "Kolmivaiheinen lataaminen saattaa parantaa latauksen suorituskykyä, mutta lisää kaistankäyttöä huomattavasti.",
"theme_setting_three_stage_loading_title": "Ota kolmivaiheinen lataus käyttöön",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_cancel": "Peruuta",
"upload_dialog_info": "Haluatko varmuuskopioida valitut kohteet palvelimelle?",
"upload_dialog_ok": "Lähetä",
"upload_dialog_title": "Lähetä kohde",
"version_announcement_overlay_ack": "Tiedostan",
"version_announcement_overlay_release_notes": "julkaisutiedoissa",
"version_announcement_overlay_text_1": "Hei, kaveri! Uusi palvelinversio on saatavilla sovelluksesta",

View File

@@ -92,11 +92,11 @@
"backup_controller_page_uploading_file_info": "Transfert des informations du fichier",
"backup_err_only_album": "Impossible de retirer le seul album",
"backup_info_card_assets": "éléments",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "Annulé",
"backup_manual_failed": "Echec",
"backup_manual_in_progress": "Téléchargement déjà en cours. Essayez après un instant",
"backup_manual_success": "Succès ",
"backup_manual_title": "Statut du téléchargement ",
"cache_settings_album_thumbnails": "Miniatures de la page bibliothèque ({} éléments)",
"cache_settings_clear_cache_button": "Effacer le cache",
"cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.",
@@ -161,7 +161,7 @@
"home_page_building_timeline": "Construction de la chronologie",
"home_page_favorite_err_local": "Impossible d'ajouter des éléments locaux aux favoris pour le moment, étape ignorée",
"home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
"image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
"image_viewer_page_state_provider_download_success": "Téléchargement réussi",
"library_page_albums": "Albums",
@@ -172,7 +172,7 @@
"library_page_sharing": "Partage",
"library_page_sort_created": "Créations les plus récentes",
"library_page_sort_title": "Titre de l'album",
"login_disabled": "Login has been disabled",
"login_disabled": "La connexion a été désactivée ",
"login_form_api_exception": "Erreur de l'API. Veuillez vérifier l'URL du serveur et et réessayer.",
"login_form_button_text": "Connexion",
"login_form_email_hint": "votreemail@email.com",
@@ -291,10 +291,10 @@
"theme_setting_theme_title": "Thème",
"theme_setting_three_stage_loading_subtitle": "Le chargement en trois étapes peut améliorer les performances de chargement, mais entraîne une augmentation significative de la charge du réseau.",
"theme_setting_three_stage_loading_title": "Activer le chargement en trois étapes",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_cancel": "Annuler",
"upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur ?",
"upload_dialog_ok": "Télécharger ",
"upload_dialog_title": "Télécharger cet élément ",
"version_announcement_overlay_ack": "Confirmer",
"version_announcement_overlay_release_notes": "notes de mise à jour",
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",

View File

@@ -1,6 +1,6 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_album_bottom_sheet_added": "Hozzáadva a(z) {album} nevű albumhoz",
"add_to_album_bottom_sheet_already_exists": "Már eleme a(z) {album} nevű albumnak",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_tile_subtitle": "Haladó felhasználói beállítások",
@@ -9,107 +9,107 @@
"advanced_settings_troubleshooting_title": "Hibaelhárítás",
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"album_thumbnail_card_item": "1 elem",
"album_thumbnail_card_items": "{} elem",
"album_thumbnail_card_shared": Megosztott",
"album_thumbnail_owned": "Tulajdonos",
"album_thumbnail_shared_by": "Megosztotta: {}",
"album_viewer_appbar_share_delete": "Delete album",
"album_viewer_appbar_share_err_delete": "Failed to delete album",
"album_viewer_appbar_share_err_leave": "Failed to leave album",
"album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
"album_viewer_appbar_share_err_title": "Failed to change album title",
"album_viewer_appbar_share_leave": "Leave album",
"album_viewer_appbar_share_remove": "Remove from album",
"album_viewer_page_share_add_users": "Add users",
"all_people_page_title": "People",
"album_viewer_appbar_share_delete": "Album törlése",
"album_viewer_appbar_share_err_delete": "Hiba az album törlése közben",
"album_viewer_appbar_share_err_leave": "Hiba az albumból való kilépés közben",
"album_viewer_appbar_share_err_remove": "Hiba az elemek törlése közben",
"album_viewer_appbar_share_err_title": "Hiba az album átnevezése közben",
"album_viewer_appbar_share_leave": "Kilépés az albumból",
"album_viewer_appbar_share_remove": "Törlés az albumból",
"album_viewer_page_share_add_users": "Felhasználók hozzáadása",
"all_people_page_title": "Emberek",
"all_videos_page_title": "Videók",
"archive_page_no_archived_assets": "Nem található archivált média",
"archive_page_title": "Archívum ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_automatically": "Automatikus",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_layout_settings_group_by_month": "Hónap",
"asset_list_layout_settings_group_by_month_day": "Hónap + nap",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Albums on device ({})",
"backup_album_selection_page_albums_device": "Az eszközön lévő albumok ({})",
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
"backup_album_selection_page_select_albums": "Select albums",
"backup_album_selection_page_select_albums": "Albumok kiválasztása",
"backup_album_selection_page_selection_info": "Selection Info",
"backup_album_selection_page_total_assets": "Total unique assets",
"backup_all": "All",
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
"backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "Checking for new assets…",
"backup_background_service_error_title": "Backup error",
"backup_background_service_in_progress_notification": "Backing up your assets…",
"backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "Backup Albums",
"backup_album_selection_page_total_assets": "Összes egyedi elem",
"backup_all": "Összes",
"backup_background_service_backup_failed_message": "HIba a mentés közben. Újrapróbálkozás...",
"backup_background_service_connection_failed_message": "HIba a szerverhez való csatlakozás közben. Újrapróbálkozás...",
"backup_background_service_current_upload_notification": "Feltöltés {}",
"backup_background_service_default_notification": "Keresés új elemek után...",
"backup_background_service_error_title": "Hiba mentés közben",
"backup_background_service_in_progress_notification": "Elemek mentés alatt..",
"backup_background_service_upload_failure_notification": "Hiba feltöltés közben {}",
"backup_controller_page_albums": "Albumok mentése",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_background_app_refresh_enable_button_text": "Beállítások megnyitása",
"backup_controller_page_background_battery_info_link": "Show me how",
"backup_controller_page_background_battery_info_link": "Mutasd meg hogyan",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Battery optimizations",
"backup_controller_page_background_charging": "Only while charging",
"backup_controller_page_background_battery_info_title": "Akkumulátoroptimalizálás",
"backup_controller_page_background_charging": "Csak töltés közben",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app",
"backup_controller_page_background_is_off": "Automatic background backup is off",
"backup_controller_page_background_is_on": "Automatic background backup is on",
"backup_controller_page_background_turn_off": "Turn off background service",
"backup_controller_page_background_turn_on": "Turn on background service",
"backup_controller_page_background_wifi": "Only on WiFi",
"backup_controller_page_backup": "Backup",
"backup_controller_page_backup_selected": "Selected: ",
"backup_controller_page_backup_sub": "Backed up photos and videos",
"backup_controller_page_cancel": "Cancel",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_background_description": "Kapcsold be a háttérfolyamatot, hogy automatikusan mentsen elemeket az applikáció megnyitása nélkül",
"backup_controller_page_background_is_off": "Automatikus mentés a háttérben ki van kapcsolva",
"backup_controller_page_background_is_on": "Automatikus mentés a háttérben bekapcsolva",
"backup_controller_page_background_turn_off": "Háttérfolyamat kikapcsolása",
"backup_controller_page_background_turn_on": "Háttérfolyamat bekapcsolása",
"backup_controller_page_background_wifi": "Csak WiFi-n",
"backup_controller_page_backup": "Mentés",
"backup_controller_page_backup_selected": "Kiválasztva:",
"backup_controller_page_backup_sub": "Mentett fotók és videók",
"backup_controller_page_cancel": "Megszakít",
"backup_controller_page_created": "Létrehozva: {}",
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
"backup_controller_page_excluded": "Excluded: ",
"backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "File name: {} [{}]",
"backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "Backup Information",
"backup_controller_page_none_selected": "None selected",
"backup_controller_page_remainder": "Remainder",
"backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
"backup_controller_page_select": "Select",
"backup_controller_page_server_storage": "Server Storage",
"backup_controller_page_start_backup": "Start Backup",
"backup_controller_page_status_off": "Automatic foreground backup is off",
"backup_controller_page_status_on": "Automatic foreground backup is on",
"backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "Albums to be backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
"backup_controller_page_excluded": "Kivéve:",
"backup_controller_page_failed": "Sikertelen ({})",
"backup_controller_page_filename": "Fájlnév: {}[{}]",
"backup_controller_page_id": "Azonosító: {}",
"backup_controller_page_info": "Mentésinformációk",
"backup_controller_page_none_selected": "Egy sincs kiválasztva",
"backup_controller_page_remainder": "Maradék",
"backup_controller_page_remainder_sub": "Hátralévő fotók és videók a kijelöltek közül",
"backup_controller_page_select": "Kiválaszt",
"backup_controller_page_server_storage": "Szerver tárhely",
"backup_controller_page_start_backup": "Mentés elindítása",
"backup_controller_page_status_off": "Autoatikus mentés az előtérben kikapcsolva",
"backup_controller_page_status_on": "Autoatikus mentés az előtérben bekapcsolva",
"backup_controller_page_storage_format": "{} / {} felhasználva",
"backup_controller_page_to_backup": "Albumok amiket mentesz",
"backup_controller_page_total": "Összes",
"backup_controller_page_total_sub": "Minden egyedi fotó és videó a kijelölt albumokból",
"backup_controller_page_turn_off": "Turn off foreground backup",
"backup_controller_page_turn_on": "Turn on foreground backup",
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "Cannot remove the only album",
"backup_info_card_assets": "assets",
"backup_manual_cancelled": "Cancelled",
"backup_err_only_album": "Az utolsó albumot nem tudod törölni",
"backup_info_card_assets": "elemek",
"backup_manual_cancelled": "Megszakítva",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_success": "Sikeres",
"backup_manual_title": "Upload status",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "Clear cache",
"cache_settings_clear_cache_button": "Gyorsítótár törlése",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_full": "Teljes képek",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Thumbnails",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_statistics_thumbnail": "Előnézeti képek",
"cache_settings_statistics_title": "Gyorsítótár által használt terület",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_title": "Caching Settings",
"cache_settings_title": "Gyorsítótár beállítások",
"change_password_form_confirm_password": "Jelszó Megerősítése",
"change_password_form_description": "Kedves {lastName} {firstName}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséfes a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.",
"change_password_form_new_password": "Új Jelszó",
@@ -118,42 +118,42 @@
"common_add_to_album": "Albumhoz ad",
"common_change_password": "Jelszócsere",
"common_create_new_album": "Új album létrehozása",
"common_server_error": "Kérjük, ellenőrid a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az app és a szerver kompatibilis verziójú legyen.",
"common_server_error": "Kérjük, ellenőrizd a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az app és a szerver kompatibilis verziójú legyen.",
"common_shared": "Megosztva",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_add_to_album": "Hozzáadás az albumhoz",
"control_bottom_app_bar_album_info": "{} elem",
"control_bottom_app_bar_album_info_shared": "{} elemek· Megosztva",
"control_bottom_app_bar_archive": "Archivál",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete": "Delete",
"control_bottom_app_bar_create_new_album": "Album létrehozása",
"control_bottom_app_bar_delete": "Törlés",
"control_bottom_app_bar_favorite": "Kedvenc",
"control_bottom_app_bar_share": "Share",
"control_bottom_app_bar_share": "Megosztás",
"control_bottom_app_bar_unarchive": "Archiválás megszüntetése",
"create_album_page_untitled": "Untitled",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share": "Share",
"create_shared_album_page_share_add_assets": "ADD ASSETS",
"create_shared_album_page_share_select_photos": "Select Photos",
"create_album_page_untitled": "Névtelen",
"create_shared_album_page_create": "Létrehoz",
"create_shared_album_page_share": "Megosztás",
"create_shared_album_page_share_add_assets": "ELEMEK HOZZÁADÁSA",
"create_shared_album_page_share_select_photos": "Fotók kiválasztása",
"curated_location_page_title": "Helyek",
"curated_object_page_title": "Dolgok",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
"delete_dialog_cancel": "Cancel",
"delete_dialog_ok": "Delete",
"delete_dialog_title": "Delete Permanently",
"delete_dialog_alert": "Ezek az elemek véglegesen törölve lesznek Immich-ről és az eszközödről is",
"delete_dialog_cancel": "Mégse",
"delete_dialog_ok": "Törlés",
"delete_dialog_title": "Törlés véglegesen",
"description_input_hint_text": "Leírás hozzáadása...",
"description_input_submit_error": "Nem sikerült frissíteni a leírást. További információért kérjük, nézd meg az eseménynaplót",
"exif_bottom_sheet_description": "Add Description...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"exif_bottom_sheet_description": "Leírás hozzáadása...",
"exif_bottom_sheet_details": "RÉSZLETEK",
"exif_bottom_sheet_location": "HELYSZÍN",
"experimental_settings_new_asset_list_subtitle": "Fejlesztés alatt",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"experimental_settings_subtitle": "Csak saját felelősségre használd",
"experimental_settings_title": "Kísérleti",
"favorites_page_no_favorites": "Nem található kedvencnek jelölt média",
"favorites_page_title": "Favorites",
"favorites_page_title": "Kedvencek",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Helyi médiát még nem lehet albumba tenni. Kihagyjuk.",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
@@ -161,42 +161,42 @@
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Helyi médiát még nem lehet a kedvencek közé tenni. Kihagyjuk.",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_upload_err_limit": "Csak 30 elemet tudsz egyszerre feltölteni, átugrás",
"image_viewer_page_state_provider_download_error": "Letöltési Hiba",
"image_viewer_page_state_provider_download_success": "Letöltés Sikeres",
"library_page_albums": "Albums",
"library_page_albums": "Albumok",
"library_page_archive": "Archívum",
"library_page_device_albums": "Albumok az Eszközön",
"library_page_favorites": "Favorites",
"library_page_new_album": "New album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"login_disabled": "Login has been disabled",
"library_page_favorites": "Kedvencek",
"library_page_new_album": "Új album",
"library_page_sharing": "Megosztás\n",
"library_page_sort_created": "Legutoljára létrehozott",
"library_page_sort_title": "Album címe",
"login_disabled": "A bejelentkezés letiltva",
"login_form_api_exception": "API hiba. Kérljük, ellenőrid a szerver címét, majd próbáld újra.",
"login_form_button_text": "Login",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_err_http": "Please specify http:// or https://",
"login_form_err_invalid_email": "Invalid Email",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_leading_whitespace": "Leading whitespace",
"login_form_err_trailing_whitespace": "Trailing whitespace",
"login_form_button_text": "Belépés",
"login_form_email_hint": "teemailed@email.com",
"login_form_endpoint_hint": "http://szerver-címe:port/api",
"login_form_endpoint_url": "Kiszolgáló végpont címe",
"login_form_err_http": "Kérem, adjon meg egy http:// vagy https:// címet",
"login_form_err_invalid_email": "Érvénytelen email cím",
"login_form_err_invalid_url": "Érvénytelen cím",
"login_form_err_leading_whitespace": "Az első karakter szóköz",
"login_form_err_trailing_whitespace": "Az utolsó karakter szóköz",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
"login_form_failed_login": "Error logging you in, check server URL, email and password",
"login_form_failed_login": "Hiba bejelentkezés közben, ellenőrizd a címet, email-t és a jelszót",
"login_form_label_email": "Email",
"login_form_label_password": "Password",
"login_form_label_password": "Jelszó",
"login_form_next_button": "Következő",
"login_form_password_hint": "password",
"login_form_save_login": "Stay logged in",
"login_form_password_hint": "jelszó",
"login_form_save_login": "Maradjon bejelentkezve",
"login_form_server_empty": "Add meg a szerver címét.",
"login_form_server_error": "Nem sikerült kapcsolódni a szerverhez.",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Mozgó Fotók",
"notification_permission_dialog_cancel": "Mégsem",
"notification_permission_dialog_content": "Az értesítések bakapcsolásához a Beállítások menüben válaszd ki az Engedélyezés-t.",
"notification_permission_dialog_content": "Az értesítések bekapcsolásához a Beállítások menüben válaszd ki az Engedélyezés-t.",
"notification_permission_dialog_settings": "Beállítások",
"notification_permission_list_tile_content": "Értesítések engedélyezése",
"notification_permission_list_tile_enable_button": "Értesítések Bekapcsolása",
@@ -215,36 +215,36 @@
"permission_onboarding_go_to_settings": "Beállítások megnyitása",
"permission_onboarding_grant_permission": "Engedélyezés",
"permission_onboarding_log_out": "Kijelentkezés",
"permission_onboarding_permission_denied": "Hozzáférés megtagadva. Az Immich használatához enedélyezni kell a fotó és videó hozzáférést a Beállításokban.",
"permission_onboarding_permission_denied": "Hozzáférés megtagadva. Az Immich használatához engedélyezni kell a fotó és videó hozzáférést a Beállításokban.",
"permission_onboarding_permission_granted": "Hozzáférés engedélyezve! Minden készen áll.",
"permission_onboarding_permission_limited": "Korlátozott hozzáférés. Ha szeretnéd, hogy az Immich a teljes galéria gyűjteményedet mentse és kezelje, akkor a Beállításokban engedélyezd a fotó és videó jogosultságokat.",
"permission_onboarding_request": "Engedélyezni kell, hogy az Immich hozzáférjen a képekhez és videókhoz",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_settings": "Settings",
"profile_drawer_sign_out": "Sign Out",
"profile_drawer_app_logs": "Naplók",
"profile_drawer_client_server_up_to_date": "Kliens és a szerver is naprakész",
"profile_drawer_settings": "Beállítások",
"profile_drawer_sign_out": "Kijelentkezés",
"recently_added_page_title": "Nemrég Hozzáadott",
"search_bar_hint": "Search your photos",
"search_bar_hint": "Keress a fotóid között",
"search_page_categories": "Kategóriák",
"search_page_favorites": "Kedvencek",
"search_page_motion_photos": "Mozgó Fotók",
"search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available",
"search_page_people": "People",
"search_page_places": "Places",
"search_page_no_places": "Helyinformáció nem érhető el",
"search_page_people": "Emberek",
"search_page_places": "Helyszínek",
"search_page_recently_added": "Nemrég hozzáadott",
"search_page_screenshots": "Képernyőképek",
"search_page_selfies": "Szelfik",
"search_page_things": "Things",
"search_page_things": "Dolgok",
"search_page_videos": "Videók",
"search_page_view_all_button": "Összes mutatása",
"search_page_your_activity": "Tevékenységeid",
"search_result_page_new_search_hint": "New Search",
"search_result_page_new_search_hint": "Új keresés",
"search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz",
"search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés",
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
"select_user_for_sharing_page_err_album": "Failed to create album",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"select_additional_user_for_sharing_page_suggestions": "Javaslatok",
"select_user_for_sharing_page_err_album": "Hiba az album létrehozása közben",
"select_user_for_sharing_page_share_suggestions": "Javaslatok",
"server_info_box_app_version": "Alkalmazás Verzió",
"server_info_box_server_version": "Szerver Verzió",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
@@ -253,52 +253,52 @@
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "immediately",
"setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "never",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_notify_hours": "{} óra",
"setting_notifications_notify_immediately": "azonnal",
"setting_notifications_notify_minutes": "{} perc",
"setting_notifications_notify_never": "soha",
"setting_notifications_notify_seconds": "{} másodperc",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
"setting_notifications_single_progress_title": "Show background backup detail progress",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
"setting_notifications_title": "Értesítések",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
"share_create_album": "Create album",
"share_dialog_preparing": "Preparing...",
"share_invite": "Invite to album",
"sharing_page_album": "Shared albums",
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
"sharing_page_empty_list": "EMPTY LIST",
"sharing_silver_appbar_create_shared_album": "Create shared album",
"sharing_silver_appbar_share_partner": "Share with partner",
"tab_controller_nav_library": "Library",
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search",
"tab_controller_nav_sharing": "Sharing",
"setting_pages_app_bar_settings": "Beállítások",
"settings_require_restart": "Kérlek indítsd újra az Immich-et hogy alkalmazd ezt a beállítást",
"share_add": "Hozzáadás",
"share_add_photos": "Fotók hozzáadása",
"share_add_title": "Cím hozzáadása",
"share_create_album": "Album létrehozása",
"share_dialog_preparing": "Előkészítés...",
"share_invite": "Meghívás az albumba",
"sharing_page_album": "Megosztott albumok",
"sharing_page_description": "Hozzon létre megosztott albumokat, hogy megoszthasson fényképeket és videókat a hálózatában lévő emberekkel.",
"sharing_page_empty_list": "ÜRES LISTA",
"sharing_silver_appbar_create_shared_album": "Megosztott album létrehozása",
"sharing_silver_appbar_share_partner": "Megosztás másokkal",
"tab_controller_nav_library": "Könyvtár",
"tab_controller_nav_photos": "Képek",
"tab_controller_nav_search": "Keresés",
"tab_controller_nav_sharing": "Megosztás",
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_dark_mode_switch": "Sötét mód",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_system_theme_switch": "Automatikus (követi a rendszer témáját)",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_title": "Téma",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"version_announcement_overlay_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
"upload_dialog_cancel": "Mégse",
"upload_dialog_info": "Akarod menteni a kiválasztott eleme(ke)t a szerverre?",
"upload_dialog_ok": "Feltöltés",
"upload_dialog_title": "Elem feltöltése",
"version_announcement_overlay_ack": "Megértettem",
"version_announcement_overlay_release_notes": "a változtatások listáját elolvasd",
"version_announcement_overlay_text_1": "Szia, egy új verzió érhető el",
"version_announcement_overlay_text_2": "kérlek szánj időt arra, hogy ",
"version_announcement_overlay_text_3": "és gyöződj meg róla, hogy a docker-compose és .env beállításai naprakészek és pontosak, különösen akkor, ha használsz watchtower-t vagy bármi olyan megoldást ami automatikusan frissíti a szervert.",
"version_announcement_overlay_title": "Új szerververzió érhető el \uD83C\uDF89"
}

View File

@@ -1,8 +1,8 @@
{
"add_to_album_bottom_sheet_added": "Aggiunto in {album}",
"add_to_album_bottom_sheet_already_exists": "Già presente in {album}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.",
"advanced_settings_prefer_remote_title": "Preferisci immagini remote.",
"advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti",
"advanced_settings_tile_title": "Avanzato",
"advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi",
@@ -22,7 +22,7 @@
"album_viewer_appbar_share_leave": "Lascia album",
"album_viewer_appbar_share_remove": "Rimuovere dall'album ",
"album_viewer_page_share_add_users": "Aggiungi utenti",
"all_people_page_title": "People",
"all_people_page_title": "Persone",
"all_videos_page_title": "Video",
"archive_page_no_archived_assets": "Nessuna oggetto archiviato",
"archive_page_title": "Archivia ({})",
@@ -33,7 +33,7 @@
"asset_list_layout_settings_group_by_month_day": "Mese + giorno",
"asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto",
"asset_list_settings_title": "Griglia foto",
"backup_album_selection_page_albums_device": "Albums sul device ({})",
"backup_album_selection_page_albums_device": "Album sul dispositivo ({})",
"backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.",
"backup_album_selection_page_assets_scatter": "Stesse immagini e video possono trovarsi tra più album, così gli album possono essere inclusi o esclusi dal backup.",
"backup_album_selection_page_select_albums": "Seleziona gli album",
@@ -44,8 +44,8 @@
"backup_background_service_connection_failed_message": "Impossibile connettersi al server. Riprovo…",
"backup_background_service_current_upload_notification": "Caricamento {}",
"backup_background_service_default_notification": "Ricerca di nuovi contenuti…",
"backup_background_service_error_title": "Errore di Backup",
"backup_background_service_in_progress_notification": "Backing dei tuoi contenuti…",
"backup_background_service_error_title": "Errore di backup",
"backup_background_service_in_progress_notification": "Backup dei tuoi contenuti…",
"backup_background_service_upload_failure_notification": "Impossibile caricare {}",
"backup_controller_page_albums": "Backup Album",
"backup_controller_page_background_app_refresh_disabled_content": "Attiva background app refresh dalle Impostazioni > Generale > Background App Refresh per utilizzare backup in background.",
@@ -69,7 +69,7 @@
"backup_controller_page_backup_sub": "Foto e video caricati",
"backup_controller_page_cancel": "Cancella ",
"backup_controller_page_created": "Creato il: {}",
"backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server",
"backup_controller_page_desc_backup": "Attiva il backup per eseguire il caricamento automatico sul server all'apertura dell'applicazione.",
"backup_controller_page_excluded": "Esclusi:",
"backup_controller_page_failed": "Falliti: ({})",
"backup_controller_page_filename": "Nome del file: {} [{}]",
@@ -77,7 +77,7 @@
"backup_controller_page_info": "Informazioni sul backup",
"backup_controller_page_none_selected": "Nessuna selezione",
"backup_controller_page_remainder": "Promemoria ",
"backup_controller_page_remainder_sub": "Photo e album selezionati che rimangono da caricare",
"backup_controller_page_remainder_sub": "Foto e album selezionati che rimangono da caricare",
"backup_controller_page_select": "Seleziona ",
"backup_controller_page_server_storage": "Spazio sul server",
"backup_controller_page_start_backup": "Inizia backup ",
@@ -92,11 +92,11 @@
"backup_controller_page_uploading_file_info": "Caricando informazioni sul file",
"backup_err_only_album": "Non è possibile rimuovere l'unico album",
"backup_info_card_assets": "oggetti ",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "Annullato",
"backup_manual_failed": "Fallito",
"backup_manual_in_progress": "Caricamento già in corso. Riprova più tardi.",
"backup_manual_success": "Successo",
"backup_manual_title": "Stato del caricamento",
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
"cache_settings_clear_cache_button": "Cancella cache",
"cache_settings_clear_cache_button_title": "Cancella la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.",
@@ -161,7 +161,7 @@
"home_page_building_timeline": "Costruendo il Timeline",
"home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate",
"home_page_first_time_notice": "Se è la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso",
"image_viewer_page_state_provider_download_error": "Errore nel Download",
"image_viewer_page_state_provider_download_success": "Download con successo",
"library_page_albums": "Album",
@@ -172,7 +172,7 @@
"library_page_sharing": "Condividendo",
"library_page_sort_created": "Creato il più recente",
"library_page_sort_title": "Titolo album",
"login_disabled": "Login has been disabled",
"login_disabled": "L'accesso è stato disattivato",
"login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi",
"login_form_button_text": "Login",
"login_form_email_hint": "tuaemail@email.com",
@@ -201,14 +201,14 @@
"notification_permission_list_tile_content": "Concedi i permessi per attivare le notifiche",
"notification_permission_list_tile_enable_button": "Attiva notifiche",
"notification_permission_list_tile_title": "Permessi delle Notifiche",
"partner_page_add_partner": "Add partner",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_select_partner": "Select partner",
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_add_partner": "Aggiungi partner.",
"partner_page_empty_message": "Le tue foto non sono ancora condivise con alcun partner.",
"partner_page_no_more_users": "Nessun altro utente da aggiungere.",
"partner_page_partner_add_failed": "Aggiunta del partner non riuscita.",
"partner_page_select_partner": "Seleziona partner.",
"partner_page_shared_to_title": "Condividi con",
"partner_page_stop_sharing_content": "{} non sarà più in grado di accedere alle tue foto.",
"partner_page_stop_sharing_title": "Stoppare la condivisione delle tue foto?",
"partner_page_title": "Partner",
"permission_onboarding_continue_anyway": "Continua lo stesso",
"permission_onboarding_get_started": "Inizia",
@@ -230,7 +230,7 @@
"search_page_motion_photos": "Motion Foto",
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
"search_page_no_places": "Nessun informazione sul luogo disponibile",
"search_page_people": "People",
"search_page_people": "Persone",
"search_page_places": "Luoghi",
"search_page_recently_added": "Aggiunte di recente",
"search_page_screenshots": "Screenshot",
@@ -291,10 +291,10 @@
"theme_setting_theme_title": "Tema",
"theme_setting_three_stage_loading_subtitle": "Il caricamento a tre stage aumenterà le performance di caricamento ma anche il consumo di banda",
"theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_cancel": "Cancella",
"upload_dialog_info": "Vuoi fare il backup sul server di ciò che hai selezionato?",
"upload_dialog_ok": "Carica",
"upload_dialog_title": "Carica file",
"version_announcement_overlay_ack": "Presa visione",
"version_announcement_overlay_release_notes": "note di rilascio ",
"version_announcement_overlay_text_1": "Ciao, c'è una nuova versione di",

View File

@@ -92,7 +92,7 @@
"backup_controller_page_uploading_file_info": "Laster opp filinformasjon",
"backup_err_only_album": "Kan ikke fjerne det eneste albumet",
"backup_info_card_assets": "objekter",
"backup_manual_cancelled": "Cancelled",
"backup_manual_cancelled": "Avbrutt",
"backup_manual_failed": "Feilet",
"backup_manual_in_progress": "Opplasting er allerede i gang. Prøv igjen om litt",
"backup_manual_success": "Vellykket",

View File

@@ -92,11 +92,11 @@
"backup_controller_page_uploading_file_info": "Bestandsgegevens uploaden",
"backup_err_only_album": "Kan het enige album niet verwijderen",
"backup_info_card_assets": "items",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "Geannuleerd",
"backup_manual_failed": "Gefaald",
"backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje",
"backup_manual_success": "Succes",
"backup_manual_title": "Uploadstatus",
"cache_settings_album_thumbnails": "Thumbnails bibliotheekpagina ({} items)",
"cache_settings_clear_cache_button": "Cache wissen",
"cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.",
@@ -161,7 +161,7 @@
"home_page_building_timeline": "Tijdlijn opbouwen",
"home_page_favorite_err_local": "Lokale items kunnen nog niet als favoriet worden aangemerkt, overslaan",
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_upload_err_limit": "Kan maximaal 30 assets tegelijk uploaden, overslaan",
"image_viewer_page_state_provider_download_error": "Download mislukt",
"image_viewer_page_state_provider_download_success": "Download succesvol",
"library_page_albums": "Albums",
@@ -172,7 +172,7 @@
"library_page_sharing": "Gedeeld",
"library_page_sort_created": "Meest recent gemaakt",
"library_page_sort_title": "Albumtitel",
"login_disabled": "Login has been disabled",
"login_disabled": "Aanmelding uitgeschakeld",
"login_form_api_exception": "API fout. Controleer de server URL en probeer opnieuw.",
"login_form_button_text": "Inloggen",
"login_form_email_hint": "jouwemail@email.com",
@@ -291,10 +291,10 @@
"theme_setting_theme_title": "Thema",
"theme_setting_three_stage_loading_subtitle": "Laden in drie fasen kan de laadprestaties verbeteren, maar veroorzaakt een aanzienlijk hogere netwerkbelasting",
"theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_cancel": "Annuleren",
"upload_dialog_info": "Wilt u een backup maken van de geselecteerde Asset(s) op de server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_title": "Asset uploaden",
"version_announcement_overlay_ack": "Bevestig",
"version_announcement_overlay_release_notes": "releaseopmerkingen",
"version_announcement_overlay_text_1": "Hoi, er is een nieuwe versie beschikbaar van",

View File

@@ -1,19 +1,19 @@
{
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"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",
"add_to_album_bottom_sheet_added": "Dodano do {album}",
"add_to_album_bottom_sheet_already_exists": "Już w {album}",
"advanced_settings_prefer_remote_subtitle": "Niektóre urządzenia bardzo wolno ładują miniatury z zasobów na urządzeniu. Aktywuj to ustawienie, aby ładować zdalne obrazy.",
"advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne",
"advanced_settings_tile_subtitle": "Zaawansowane ustawienia użytkownika",
"advanced_settings_tile_title": "Zaawansowane",
"advanced_settings_troubleshooting_subtitle": "Włącz dodatkowe funkcje rozwiązywania problemów",
"advanced_settings_troubleshooting_title": "Rozwiązywanie problemów",
"album_info_card_backup_album_excluded": "WYKLUCZONE",
"album_info_card_backup_album_included": "WŁĄCZONE",
"album_thumbnail_card_item": "1 pozycja",
"album_thumbnail_card_items": "{} pozycje",
"album_thumbnail_card_shared": "Udostępniony",
"album_thumbnail_owned": "Owned",
"album_thumbnail_shared_by": "Shared by {}",
"album_thumbnail_owned": "Posiadany",
"album_thumbnail_shared_by": "Udostępnione przez {}",
"album_viewer_appbar_share_delete": "Usuń album",
"album_viewer_appbar_share_err_delete": "Nie udało się usunąć albumu",
"album_viewer_appbar_share_err_leave": "Nie udało się wyjść z albumu",
@@ -22,15 +22,15 @@
"album_viewer_appbar_share_leave": "Opuść album",
"album_viewer_appbar_share_remove": "Usuń z albumu",
"album_viewer_page_share_add_users": "Dodaj użytkowników",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"all_people_page_title": "Ludzie",
"all_videos_page_title": "Filmy",
"archive_page_no_archived_assets": "Nie znaleziono zarchiwizowanych zasobów",
"archive_page_title": "Archiwum ({})",
"asset_list_layout_settings_dynamic_layout_title": "Układ dynamiczny",
"asset_list_layout_settings_group_automatically": "Automatyczny",
"asset_list_layout_settings_group_by": "Grupuj zasoby według",
"asset_list_layout_settings_group_by_month": "Miesiąc",
"asset_list_layout_settings_group_by_month_day": "Miesiąc + dzień",
"asset_list_settings_subtitle": "Ustawienia układu siatki zdjęć",
"asset_list_settings_title": "Siatka Zdjęć",
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})",
@@ -48,16 +48,16 @@
"backup_background_service_in_progress_notification": "Tworzę kopię twoich zasobów...",
"backup_background_service_upload_failure_notification": "Nie udało się przesłać {}",
"backup_controller_page_albums": "Backup Albumów",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
"backup_controller_page_background_app_refresh_disabled_content": "Włącz odświeżanie aplikacji w tle w Ustawienia > Ogólne > Odświeżanie aplikacji w tle, aby móc korzystać z kopii zapasowej w tle.",
"backup_controller_page_background_app_refresh_disabled_title": "Odświeżanie aplikacji w tle wyłączone",
"backup_controller_page_background_app_refresh_enable_button_text": "Przejdź do ustawień",
"backup_controller_page_background_battery_info_link": "Pokaż mi jak",
"backup_controller_page_background_battery_info_message": "Aby uzyskać najlepsze rezultaty podczas tworzenia kopii zapasowej w tle, należy wyłączyć wszelkie optymalizacje baterii ograniczające aktywność w tle dla Immich w urządzeniu.\n\nPonieważ jest to zależne od urządzenia, proszę sprawdzić wymagane informacje dla producenta urządzenia.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Optymalizacja Baterii",
"backup_controller_page_background_charging": "Tylko podczas ładowania",
"backup_controller_page_background_configure_error": "Nie udało się skonfigurować usługi w tle",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_delay": "Opóźnij tworzenie kopii zapasowych nowych zasobów: {}",
"backup_controller_page_background_description": "Włącz usługę w tle, aby automatycznie tworzyć kopie zapasowe wszelkich nowych zasobów bez konieczności otwierania aplikacji",
"backup_controller_page_background_is_off": "Automatyczna kopia zapasowa w tle jest wyłączona",
"backup_controller_page_background_is_on": "Automatyczna kopia zapasowa w tle jest włączona",
@@ -92,11 +92,11 @@
"backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku",
"backup_err_only_album": "Nie można usunąć tylko i wyłącznie albumu",
"backup_info_card_assets": "zasoby",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "Anulowano",
"backup_manual_failed": "Niepowodzenie",
"backup_manual_in_progress": "Przesyłanie już trwa. Spróbuj po pewnym czasie",
"backup_manual_success": "Sukces",
"backup_manual_title": "Stan przesyłania",
"cache_settings_album_thumbnails": "Miniatury stron bibliotek ({} zasobów)",
"cache_settings_clear_cache_button": "Wyczyść Cache",
"cache_settings_clear_cache_button_title": "Czyści pamięć podręczną aplikacji. Wpłynie to znacząco na wydajność aplikacji, dopóki pamięć podręczna nie zostanie odbudowana.",
@@ -110,32 +110,32 @@
"cache_settings_subtitle": "Kontrolowanie zachowania buforowania aplikacji mobilnej Immich",
"cache_settings_thumbnail_size": "Rozmiar pamięci podręcznej miniatur ({} zasobów)",
"cache_settings_title": "Ustawienia Buforowania",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Add to album",
"control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Create new album",
"change_password_form_confirm_password": "Potwierdź Hasło",
"change_password_form_description": "Cześć {firstName} {lastName},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.",
"change_password_form_new_password": "Nowe Hasło",
"change_password_form_password_mismatch": "Hasła nie są zgodne",
"change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło",
"common_add_to_album": "Dodaj do albumu",
"common_change_password": "Zmień Hasło",
"common_create_new_album": "Utwórz nowy album",
"common_server_error": "Sprawdź połączenie sieciowe, upewnij się, że serwer jest osiągalny i wersje aplikacji/serwera są kompatybilne.",
"common_shared": "Udostępnione",
"control_bottom_app_bar_add_to_album": "Dodaj do albumu",
"control_bottom_app_bar_album_info": "{} pozycji",
"control_bottom_app_bar_album_info_shared": "{} pozycji · Udostępnionych",
"control_bottom_app_bar_archive": "Archiwum",
"control_bottom_app_bar_create_new_album": "Utwórz nowy album",
"control_bottom_app_bar_delete": "Usuń",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_favorite": "Ulubione",
"control_bottom_app_bar_share": "Udostępnij",
"control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_unarchive": "Cofnij archiwizację",
"create_album_page_untitled": "Bez tytułu",
"create_shared_album_page_create": "Utwórz",
"create_shared_album_page_share": "Udostępnij",
"create_shared_album_page_share_add_assets": "DODAJ ZASOBY",
"create_shared_album_page_share_select_photos": "Zaznacz Zdjęcia",
"curated_location_page_title": "Places",
"curated_object_page_title": "Things",
"curated_location_page_title": "Miejsca",
"curated_object_page_title": "Rzeczy",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
@@ -143,129 +143,129 @@
"delete_dialog_cancel": "Anuluj",
"delete_dialog_ok": "Usuń",
"delete_dialog_title": "Usuń trwale",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"description_input_hint_text": "Dodaj opis...",
"description_input_submit_error": "Błąd aktualizacji opisu, sprawdź dziennik, aby uzyskać więcej szczegółów",
"exif_bottom_sheet_description": "Dodaj Opis...",
"exif_bottom_sheet_details": "SZCZEGÓŁY",
"exif_bottom_sheet_location": "LOKALIZACJA",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favorites",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_building_timeline": "Building the timeline",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"image_viewer_page_state_provider_download_error": "Download Error",
"image_viewer_page_state_provider_download_success": "Download Success",
"experimental_settings_new_asset_list_subtitle": "Praca w toku",
"experimental_settings_new_asset_list_title": "Włącz eksperymentalną układ zdjęć",
"experimental_settings_subtitle": "Używaj na własne ryzyko!",
"experimental_settings_title": "Eksperymentalny",
"favorites_page_no_favorites": "Nie znaleziono ulubionych zasobów",
"favorites_page_title": "Ulubione",
"home_page_add_to_album_conflicts": "Dodano {added} zasoby do albumu {album}. {failed} zasobów jest już w albumie.",
"home_page_add_to_album_err_local": "Nie można dodawać zasobów lokalnych do albumów, pomijam",
"home_page_add_to_album_success": "Dodano {added} zasoby do albumu {album}.",
"home_page_archive_err_local": "Nie można jeszcze zarchiwizować zasobów lokalnych, pomijanie",
"home_page_building_timeline": "Budowanie osi czasu",
"home_page_favorite_err_local": "Nie można dodać do ulubionych lokalnych zasobów, pomijam",
"home_page_first_time_notice": "Jeśli korzystasz z aplikacji po raz pierwszy, pamiętaj o wybraniu albumów zapasowych, aby oś czasu mogła zapełnić zdjęcia i filmy w albumach.",
"home_page_upload_err_limit": "Można przesłać maksymalnie 30 zasobów jednocześnie, pomijanie",
"image_viewer_page_state_provider_download_error": "Błąd pobierania",
"image_viewer_page_state_provider_download_success": "Pobieranie zakończone",
"library_page_albums": "Albumy",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favorites",
"library_page_archive": "Archiwum",
"library_page_device_albums": "Albumy na Urządzeniu",
"library_page_favorites": "Ulubione",
"library_page_new_album": "Nowy album",
"library_page_sharing": "Sharing",
"library_page_sort_created": "Most recently created",
"library_page_sort_title": "Album title",
"login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"library_page_sharing": "Udostępnianie",
"library_page_sort_created": "Ostatnio utworzone",
"library_page_sort_title": "Tytuł albumu",
"login_disabled": "Logowanie zostało wyłączone",
"login_form_api_exception": "Wyjątek API. Sprawdź adres URL serwera i spróbuj ponownie.",
"login_form_button_text": "Login",
"login_form_email_hint": "twojmail@email.com",
"login_form_endpoint_hint": "http://ip-twojego-serwera:port/api",
"login_form_endpoint_url": "URL Serwera",
"login_form_err_http": "Proszę określić http:// lub https://",
"login_form_err_invalid_email": "Niepoprawny Email",
"login_form_err_invalid_url": "Invalid URL",
"login_form_err_invalid_url": "Nieprawidłowy URL",
"login_form_err_leading_whitespace": "Białe znaki",
"login_form_err_trailing_whitespace": "Białe znaki po przecinku",
"login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL",
"login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server",
"login_form_failed_get_oauth_server_config": "Błąd logowania przy użyciu OAuth. Sprawdź adres URL serwera",
"login_form_failed_get_oauth_server_disable": "Funkcja OAuth nie jest dostępna na tym serwerze",
"login_form_failed_login": "Błąd logowania, sprawdź adres url serwera, email i hasło.",
"login_form_label_email": "Email",
"login_form_label_password": "Hasło",
"login_form_next_button": "Next",
"login_form_next_button": "Dalej",
"login_form_password_hint": "hasło",
"login_form_save_login": "Pozostań zalogowany",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_form_server_empty": "Wprowadź adres URL serwera.",
"login_form_server_error": "Nie można połączyć się z serwerem.",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"partner_page_add_partner": "Add partner",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_select_partner": "Select partner",
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"motion_photos_page_title": "Zdjęcia ruchome",
"notification_permission_dialog_cancel": "Anuluj",
"notification_permission_dialog_content": "Aby włączyć powiadomienia, przejdź do Ustawień i wybierz opcję Zezwalaj.",
"notification_permission_dialog_settings": "Ustawienia",
"notification_permission_list_tile_content": "Przyznaj uprawnienia, aby włączyć powiadomienia.",
"notification_permission_list_tile_enable_button": "Włącz Powiadomienia",
"notification_permission_list_tile_title": "Pozwolenie na powiadomienia",
"partner_page_add_partner": "Dodaj partnera",
"partner_page_empty_message": "Twoje zdjęcia nie są udostępnione żadnemu partnerowi",
"partner_page_no_more_users": "Brak użytkowników do dodania",
"partner_page_partner_add_failed": "Nie udało się dodać partnera",
"partner_page_select_partner": "Wybierz partnera",
"partner_page_shared_to_title": "Udostępniono",
"partner_page_stop_sharing_content": "{} nie będziesz już mieć dostępu do swoich zdjęć.",
"partner_page_stop_sharing_title": "Przestać udostępniać swoje zdjęcia?",
"partner_page_title": "Partner",
"permission_onboarding_continue_anyway": "Continue anyway",
"permission_onboarding_get_started": "Get started",
"permission_onboarding_go_to_settings": "Go to settings",
"permission_onboarding_grant_permission": "Grant permission",
"permission_onboarding_log_out": "Log out",
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"profile_drawer_app_logs": "Logs",
"permission_onboarding_continue_anyway": "Kontynuuj mimo to",
"permission_onboarding_get_started": "Rozpocznij",
"permission_onboarding_go_to_settings": "Przejdź do ustawień",
"permission_onboarding_grant_permission": "Wydaj pozwolenie",
"permission_onboarding_log_out": "Wyloguj",
"permission_onboarding_permission_denied": "Odmowa pozwolenia. Aby korzystać z Immich, przyznaj uprawnienia do zdjęć i filmów w Ustawieniach.",
"permission_onboarding_permission_granted": "Pozwolenie udzielone! Wszystko gotowe.",
"permission_onboarding_permission_limited": "Pozwolenie ograniczone. Aby umożliwić Immichowi tworzenie kopii zapasowych całej kolekcji galerii i zarządzanie nią, przyznaj uprawnienia do zdjęć i filmów w Ustawieniach.",
"permission_onboarding_request": "Immich potrzebuje pozwolenia na przeglądanie Twoich zdjęć i filmów.",
"profile_drawer_app_logs": "Logi",
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
"profile_drawer_settings": "Ustawienia",
"profile_drawer_sign_out": "Wyloguj się",
"recently_added_page_title": "Recently Added",
"recently_added_page_title": "Ostatnio Dodane",
"search_bar_hint": "Szukaj swoich zdjęć",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
"search_page_categories": "Kategorie",
"search_page_favorites": "Ulubione",
"search_page_motion_photos": "Zdjęcia ruchome",
"search_page_no_objects": "Brak informacji o obiektach",
"search_page_no_places": "Brak informacji o miejscu",
"search_page_people": "People",
"search_page_people": "Ludzie",
"search_page_places": "Miejsca",
"search_page_recently_added": "Recently added",
"search_page_screenshots": "Screenshots",
"search_page_selfies": "Selfies",
"search_page_recently_added": "Ostatnio dodane",
"search_page_screenshots": "Zrzuty ekranu",
"search_page_selfies": "Selfi",
"search_page_things": "Rzeczy",
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_videos": "Filmy",
"search_page_view_all_button": "Pokaż wszystkie",
"search_page_your_activity": "Twoja aktywność",
"search_result_page_new_search_hint": "Nowe wyszukiwanie",
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
"search_suggestion_list_smart_search_hint_1": "Inteligentne wyszukiwanie jest domyślnie włączone, aby wyszukiwać metadane, użyj składni ",
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
"select_additional_user_for_sharing_page_suggestions": "Propozycje",
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
"select_user_for_sharing_page_share_suggestions": "Propozycje",
"server_info_box_app_version": "App Version",
"server_info_box_server_version": "Server Version",
"setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).",
"setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).",
"setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image",
"server_info_box_app_version": "Wersja Aplikacji",
"server_info_box_server_version": "Wersja Serwera",
"setting_image_viewer_help": "Przeglądarka szczegółów najpierw ładuje małą miniaturę, następnie ładuje podgląd średniej wielkości (jeśli jest włączony), a na koniec ładuje oryginał (jeśli jest włączony).",
"setting_image_viewer_original_subtitle": "Włącz ładowanie oryginalnego obrazu w pełnej rozdzielczości (dużego!). Wyłącz, aby zmniejszyć zużycie danych (zarówno w sieci, jak i w pamięci podręcznej urządzenia).",
"setting_image_viewer_original_title": "Załaduj oryginalny obraz",
"setting_image_viewer_preview_subtitle": "Włącz ładowanie obrazu o średniej rozdzielczości. Wyłącz opcję bezpośredniego ładowania oryginału lub używania tylko miniatury.",
"setting_image_viewer_preview_title": "Załaduj obraz podglądu",
"setting_notifications_notify_failures_grace_period": "Powiadomienie o awariach kopii zapasowych w tle: {}",
"setting_notifications_notify_hours": "{} godzin",
"setting_notifications_notify_immediately": "natychmiast",
"setting_notifications_notify_minutes": "{} minut",
"setting_notifications_notify_never": "nigdy",
"setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
"setting_notifications_single_progress_title": "Show background backup detail progress",
"setting_notifications_notify_seconds": "{} sekund",
"setting_notifications_single_progress_subtitle": "Szczegółowe informacje o postępie przesyłania dla każdego zasobu",
"setting_notifications_single_progress_title": "Pokaż postęp szczegółów kopii zapasowej w tle",
"setting_notifications_subtitle": "Dostosuj preferencje powiadomień",
"setting_notifications_title": "Powiadomienia",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_notifications_total_progress_subtitle": "Ogólny postęp przesyłania (gotowe/całkowite zasoby)",
"setting_notifications_total_progress_title": "Pokaż całkowity postęp tworzenia kopii zapasowej w tle",
"setting_pages_app_bar_settings": "Ustawienia",
"settings_require_restart": "Please restart Immich to apply this setting",
"settings_require_restart": "Aby zastosować to ustawienie, uruchom ponownie Immich",
"share_add": "Dodaj",
"share_add_photos": "Dodaj zdjęcia",
"share_add_title": "Dodaj tytuł",
@@ -282,7 +282,7 @@
"tab_controller_nav_search": "Szukaj",
"tab_controller_nav_sharing": "Udostępnianie",
"theme_setting_asset_list_storage_indicator_title": "Pokaż wskaźnik przechowywania na kafelkach zasobów",
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_asset_list_tiles_per_row_title": "Liczba zasobów w wierszu ({})",
"theme_setting_dark_mode_switch": "Ciemny Motyw",
"theme_setting_image_viewer_quality_subtitle": "Dostosuj jakość podglądu szczegółowości",
"theme_setting_image_viewer_quality_title": "Jakość przeglądania obrazów",
@@ -291,10 +291,10 @@
"theme_setting_theme_title": "Motyw",
"theme_setting_three_stage_loading_subtitle": "Trójstopniowe ładowanie może zwiększyć wydajność ładowania, ale powoduje znacznie większe obciążenie sieci",
"theme_setting_three_stage_loading_title": "Włączenie trójstopniowego ładowania",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_cancel": "Anuluj",
"upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobów na serwerze?",
"upload_dialog_ok": "Prześlij",
"upload_dialog_title": "Prześlij Zasób",
"version_announcement_overlay_ack": "Potwierdzam",
"version_announcement_overlay_release_notes": "informacje o wydaniu",
"version_announcement_overlay_text_1": "Cześć przyjacielu, jest nowe wydanie",

View File

@@ -1,19 +1,19 @@
{
"add_to_album_bottom_sheet_added": "Добавлено в {album}",
"add_to_album_bottom_sheet_already_exists": "Уже в {album}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"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_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают предпросмотр объектов, находящихся на устройстве. Активируйте эту настройку, чтобы вместо них загружались изображени с сервера.",
"advanced_settings_prefer_remote_title": "Предпочитать фото на сервере",
"advanced_settings_tile_subtitle": "Расширенные настройки пользователя",
"advanced_settings_tile_title": "Расширенные",
"advanced_settings_troubleshooting_subtitle": "Включить расширенные возможности для решения проблем",
"advanced_settings_troubleshooting_title": "Решение проблем",
"album_info_card_backup_album_excluded": "ИСКЛЮЧЕН",
"album_info_card_backup_album_included": "ВКЛЮЧЕН",
"album_thumbnail_card_item": "1 объект",
"album_thumbnail_card_items": "{} объектов",
"album_thumbnail_card_shared": "· Общий",
"album_thumbnail_owned": "Owned",
"album_thumbnail_shared_by": "Shared by {}",
"album_thumbnail_owned": "Автор",
"album_thumbnail_shared_by": "Поделился {}",
"album_viewer_appbar_share_delete": "Удалить альбом",
"album_viewer_appbar_share_err_delete": "Невозможно удалить альбом",
"album_viewer_appbar_share_err_leave": "Невозможно покинуть альбом",
@@ -22,15 +22,15 @@
"album_viewer_appbar_share_leave": "Покинуть альбом",
"album_viewer_appbar_share_remove": "Удалить из альбома",
"album_viewer_page_share_add_users": "Добавить пользователей",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"all_people_page_title": "Люди",
"all_videos_page_title": "Видео",
"archive_page_no_archived_assets": "В архиве сейчас пусто",
"archive_page_title": "Архив ({})",
"asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Группировать объекты по",
"asset_list_layout_settings_group_by_month": "месяцу",
"asset_list_layout_settings_group_by_month_day": "месяцу и дню",
"asset_list_layout_settings_group_automatically": "Автоматически",
"asset_list_layout_settings_group_by": "Группировать объекты по:",
"asset_list_layout_settings_group_by_month": "Месяцу",
"asset_list_layout_settings_group_by_month_day": "Месяцу и дню",
"asset_list_settings_subtitle": "Настройки макета сетки фотографий",
"asset_list_settings_title": "Сетка фотографий",
"backup_album_selection_page_albums_device": "Альбомов на устройстве ({})",
@@ -48,9 +48,9 @@
"backup_background_service_in_progress_notification": "Резервное копирование ваших объектов…",
"backup_background_service_upload_failure_notification": "Ошибка загрузки {}",
"backup_controller_page_albums": "Резервное копирование альбомов",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_background_app_refresh_enable_button_text": "Go to settings",
"backup_controller_page_background_app_refresh_disabled_content": "Включите фоновое обновление приложений в меню Настройки > Общие > Фоновое обновление приложений, чтобы использовать фоновое резервное копирование.",
"backup_controller_page_background_app_refresh_disabled_title": "Фоновое обновление отключено",
"backup_controller_page_background_app_refresh_enable_button_text": "Перейти в настройки",
"backup_controller_page_background_battery_info_link": "Показать как",
"backup_controller_page_background_battery_info_message": "Для наилучшего фонового резервного копирования отключите любые настройки оптимизации батареи, ограничивающие фоновую активность для Immich.\n\nПоскольку это зависит от устройства, найдите необходимую информацию для производителя вашего устройства.",
"backup_controller_page_background_battery_info_ok": "ОК",
@@ -68,8 +68,8 @@
"backup_controller_page_backup_selected": "Выбрано: ",
"backup_controller_page_backup_sub": "Загруженные фото и видео",
"backup_controller_page_cancel": "Отмена",
"backup_controller_page_created": "Создано на: {}",
"backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые активы на сервер при открытии приложения.",
"backup_controller_page_created": "Создано: {}",
"backup_controller_page_desc_backup": "Включите резервное копирование в активном режиме, чтобы автоматически загружать новые объекты на сервер при открытии приложения.",
"backup_controller_page_excluded": "Исключены:",
"backup_controller_page_failed": "Неудачных ({})",
"backup_controller_page_filename": "Имя файла: {} [{}]",
@@ -89,14 +89,14 @@
"backup_controller_page_total_sub": "Все уникальные фото и видео из выбранных альбомов",
"backup_controller_page_turn_off": "Выключить резервное копирование в активном режиме",
"backup_controller_page_turn_on": "Включить резервное копирование в активном режиме",
"backup_controller_page_uploading_file_info": "Загрузка информации о файле",
"backup_controller_page_uploading_file_info": "Информация о загружаемом файле",
"backup_err_only_album": "Невозможно удалить единственный альбом",
"backup_info_card_assets": "объекты",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_info_card_assets": "объектов",
"backup_manual_cancelled": "Отменено",
"backup_manual_failed": "Неудачно",
"backup_manual_in_progress": "Загрузка уже в процессе, попробуйте позже",
"backup_manual_success": "Успешно",
"backup_manual_title": "Статус загрузки",
"cache_settings_album_thumbnails": "Миниатюры страниц библиотеки ({} объектов)",
"cache_settings_clear_cache_button": "Очистить кэш",
"cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это значительно повлияет на производительность приложения, до тех пор, пока кэш не будет перестроен заново.",
@@ -118,66 +118,66 @@
"common_add_to_album": "Добавить в альбом",
"common_change_password": "Изменить пароль",
"common_create_new_album": "Создать новый альбом",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"common_server_error": "Пожалуйста, проверьте подключение к сети и убедитесь, что ваш сервер доступен, а версии приложения и сервера — совместимы.",
"common_shared": "Общие",
"control_bottom_app_bar_add_to_album": "Добавить в альбом",
"control_bottom_app_bar_album_info": "{} файлов",
"control_bottom_app_bar_album_info_shared": "{} файлов · Общий",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_archive": "Архив",
"control_bottom_app_bar_create_new_album": "\nСоздать новый альбом",
"control_bottom_app_bar_delete": "Удалить",
"control_bottom_app_bar_favorite": "Избранное",
"control_bottom_app_bar_share": "Поделиться",
"control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_unarchive": "Восстановить",
"create_album_page_untitled": "Без названия",
"create_shared_album_page_create": "Создать",
"create_shared_album_page_share": "Поделиться",
"create_shared_album_page_share_add_assets": "ДОБАВИТЬ ОБЪЕКТЫ",
"create_shared_album_page_share_select_photos": "Выберите фотографии",
"curated_location_page_title": "Places",
"curated_object_page_title": "Things",
"curated_location_page_title": "Места",
"curated_object_page_title": "Предметы",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Эти объекты будут безвозвратно удалены из приложения, а также с вашего устройства",
"delete_dialog_alert": "Эти элементы будут безвозвратно удалены из приложения, а также с вашего устройства",
"delete_dialog_cancel": "Отменить",
"delete_dialog_ok": "Удалить",
"delete_dialog_title": "Удалить навсегда",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"description_input_hint_text": "Добавить описание...",
"description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину",
"exif_bottom_sheet_description": "Добавить описание...",
"exif_bottom_sheet_details": "ПОДРОБНОСТИ",
"exif_bottom_sheet_location": "МЕСТОПОЛОЖЕНИЕ",
"experimental_settings_new_asset_list_subtitle": "Работа ведётся",
"experimental_settings_new_asset_list_subtitle": "Ведутся работы",
"experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий",
"experimental_settings_subtitle": "Используйте на свой страх и риск!",
"experimental_settings_title": "Экспериментальные функции",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_no_favorites": "В избранном сейчас пусто",
"favorites_page_title": "Избранное",
"home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.",
"home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропускаем",
"home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_archive_err_local": "Пока невозможно добавить локальные объекты в архив, пропускаем",
"home_page_building_timeline": "Построение временной шкалы",
"home_page_favorite_err_local": "Пока не удается добавить в избранное локальные объекты, пропускаем",
"home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_upload_err_limit": "Вы можете выгрузить максимум 30 файлов за раз",
"image_viewer_page_state_provider_download_error": "Ошибка загрузки",
"image_viewer_page_state_provider_download_success": "Успешно загружено",
"library_page_albums": "Альбомы",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_archive": "Архив",
"library_page_device_albums": "Альбомы на устройстве",
"library_page_favorites": "Избранное",
"library_page_new_album": "Новый альбом",
"library_page_sharing": "Общие",
"library_page_sort_created": "По новизне",
"library_page_sort_title": "По названию альбома",
"login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_disabled": "Вход отключен",
"login_form_api_exception": "Ошибка при попытке взаимодействия с сервером. Проверьте URL-адрес до него и попробуйте еще раз.",
"login_form_button_text": "Войти",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http://your-server-ip:port/api",
"login_form_endpoint_url": "URL Адрес сервера",
"login_form_endpoint_url": "URL-aдрес сервера",
"login_form_err_http": "Пожалуйста, укажите http:// или https://",
"login_form_err_invalid_email": "Неверный адрес Email",
"login_form_err_invalid_url": "Неверная ссылка",
@@ -188,60 +188,60 @@
"login_form_failed_login": "Ошибка при входе в систему, проверьте URL-адрес сервера, адрес электронной почты и пароль",
"login_form_label_email": "Email",
"login_form_label_password": "Пароль",
"login_form_next_button": "Next",
"login_form_next_button": "Далее",
"login_form_password_hint": "пароль",
"login_form_save_login": "Оставаться в системе",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_form_server_empty": "Введите URL-адрес вашего сервера.",
"login_form_server_error": "Нет соединения с сервером.",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"motion_photos_page_title": "Динамические фото",
"notification_permission_dialog_cancel": "Отмена",
"notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».",
"notification_permission_dialog_settings": "Настройки",
"notification_permission_list_tile_content": "Предоставьте разрешение на включение уведомлений",
"notification_permission_list_tile_enable_button": "Включить уведомления",
"notification_permission_list_tile_title": "Разрешение на уведомление",
"partner_page_add_partner": "Add partner",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_select_partner": "Select partner",
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_title": "Partner",
"permission_onboarding_continue_anyway": "Continue anyway",
"permission_onboarding_get_started": "Get started",
"permission_onboarding_go_to_settings": "Go to settings",
"permission_onboarding_grant_permission": "Grant permission",
"permission_onboarding_log_out": "Log out",
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"profile_drawer_app_logs": "Журналы",
"partner_page_add_partner": "Добавить партнёра",
"partner_page_empty_message": "У вашего партнёра еще пока нет доступа к вашим фото",
"partner_page_no_more_users": "Выбраны все доступные пользователи",
"partner_page_partner_add_failed": "Не удалось добавить партнёра",
"partner_page_select_partner": "Выбрать партнёра",
"partner_page_shared_to_title": "Поделиться с...",
"partner_page_stop_sharing_content": "{} больше не сможет получить доступ к вашим фотографиям",
"partner_page_stop_sharing_title": "Закрыть доступ партнёра к вашим фото?",
"partner_page_title": "Партнёр",
"permission_onboarding_continue_anyway": "Все равно продолжить",
"permission_onboarding_get_started": "Давайте начнём",
"permission_onboarding_go_to_settings": "Перейти в настройки",
"permission_onboarding_grant_permission": "Предоставить разрешение",
"permission_onboarding_log_out": "Выйти",
"permission_onboarding_permission_denied": "Не удалось получить доступ.",
"permission_onboarding_permission_granted": "Доступ получен! Всё готово.",
"permission_onboarding_permission_limited": "Доступ к файлам ограничен. Чтобы Immich мог создавать резервные копии и управлять вашей галереей, пожалуйста, предоставьте приложению разрешение на доступ к \"Фото и видео\" в Настройках.",
"permission_onboarding_request": "Immich просит вас предоставить разрешение на доступ к вашим фото и видео",
"profile_drawer_app_logs": "Журнал",
"profile_drawer_client_server_up_to_date": "Клиент и сервер обновлены",
"profile_drawer_settings": "Настройки",
"profile_drawer_sign_out": "Выйти",
"recently_added_page_title": "Recently Added",
"recently_added_page_title": "Недавно добавленные",
"search_bar_hint": "Поиск фотографий",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
"search_page_categories": "Категории",
"search_page_favorites": "Избранное",
"search_page_motion_photos": "Динамические фото",
"search_page_no_objects": "Нет доступной информации об объектах",
"search_page_no_places": "Информация о местах отсутствует",
"search_page_people": "People",
"search_page_people": "Люди",
"search_page_places": "Места",
"search_page_recently_added": "Recently added",
"search_page_screenshots": "Screenshots",
"search_page_selfies": "Selfies",
"search_page_recently_added": "Недавно добавленные",
"search_page_screenshots": "Скриншоты",
"search_page_selfies": "Селфи",
"search_page_things": "Предметы",
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_videos": "Видео",
"search_page_view_all_button": "Посмотреть все",
"search_page_your_activity": "Ваша активность",
"search_result_page_new_search_hint": "Новый поиск",
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
"search_suggestion_list_smart_search_hint_1": "Интеллектуальный поиск включен по умолчанию, для поиска метаданных используйте специальный синтаксис",
"search_suggestion_list_smart_search_hint_2": "m:ваш-запрос",
"select_additional_user_for_sharing_page_suggestions": "Предложения",
"select_user_for_sharing_page_err_album": "\nНе удалось создать альбом",
"select_user_for_sharing_page_share_suggestions": "Предложения",
@@ -276,7 +276,7 @@
"sharing_page_description": "Создавайте общие альбомы, чтобы делиться фотографиями и видео с людьми в вашей сети.",
"sharing_page_empty_list": "ПУСТОЙ СПИСОК",
"sharing_silver_appbar_create_shared_album": "Создать общий альбом",
"sharing_silver_appbar_share_partner": "Поделиться с партнером",
"sharing_silver_appbar_share_partner": "Поделиться с партнёром",
"tab_controller_nav_library": "Библиотека",
"tab_controller_nav_photos": "Фото",
"tab_controller_nav_search": "Поиск",
@@ -284,17 +284,17 @@
"theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов",
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})",
"theme_setting_dark_mode_switch": "Тёмная тема",
"theme_setting_image_viewer_quality_subtitle": "Настройте качество просмотра подробного изображения",
"theme_setting_image_viewer_quality_subtitle": "Настройка качества детального просмотра изображения",
"theme_setting_image_viewer_quality_title": "Качество просмотра изображений",
"theme_setting_system_theme_switch": "Автоматически (Как в системе)",
"theme_setting_theme_subtitle": "Выберите настройки темы приложения",
"theme_setting_theme_title": "Тема",
"theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность загрузки, но вызывает значительно более высокую нагрузку на сеть",
"theme_setting_three_stage_loading_title": "Включить трехэтапную загрузку",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"upload_dialog_cancel": "Отмена",
"upload_dialog_info": "Вы хотите загрузить выбранный объект(ы) на ваш сервер?",
"upload_dialog_ok": "Загрузить",
"upload_dialog_title": "Загрузить объект",
"version_announcement_overlay_ack": "Подтверждение",
"version_announcement_overlay_release_notes": "примечания к выпуску",
"version_announcement_overlay_text_1": "Привет друг, вышел новый релиз",

View File

@@ -269,7 +269,7 @@
"share_add": "Pridať",
"share_add_photos": "Pridať fotografie",
"share_add_title": "Pridať názov",
"share_create_album": "Tvorba albumu",
"share_create_album": "Vytvor album",
"share_dialog_preparing": "Pripravujem...",
"share_invite": "Pozvať do albumu",
"sharing_page_album": "Zdieľané albumy",

View File

@@ -92,7 +92,7 @@
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "ไม่สามารถนำอัลบั้มสุดท้ายออกได้",
"backup_info_card_assets": "ทรัพยากร",
"backup_manual_cancelled": "Cancelled",
"backup_manual_cancelled": "ถูกยกเลิก",
"backup_manual_failed": "ล้มเหลว",
"backup_manual_in_progress": "อัปโหลดกำลังดำเนินการอยู่ โปรดลองใหม่ในสักพัก",
"backup_manual_success": "สำเร็จ",

View File

@@ -77,7 +77,7 @@
"backup_controller_page_info": "备份信息",
"backup_controller_page_none_selected": "未选择",
"backup_controller_page_remainder": "剩余",
"backup_controller_page_remainder_sub": "选中的数据中尚未备份的数据",
"backup_controller_page_remainder_sub": "选数据中尚未备份的数据",
"backup_controller_page_select": "选择",
"backup_controller_page_server_storage": "服务器存储",
"backup_controller_page_start_backup": "开始备份",
@@ -89,26 +89,26 @@
"backup_controller_page_total_sub": "选中相册中的所有不重复的视频和图像",
"backup_controller_page_turn_off": "关闭前台备份",
"backup_controller_page_turn_on": "开启前台备份",
"backup_controller_page_uploading_file_info": "正在上传文件信息",
"backup_controller_page_uploading_file_info": "正在上传中的文件信息",
"backup_err_only_album": "不能移除唯一的一个相册",
"backup_info_card_assets": "",
"backup_manual_cancelled": "Cancelled",
"backup_info_card_assets": "",
"backup_manual_cancelled": "已取消",
"backup_manual_failed": "失败",
"backup_manual_in_progress": "上传正在进行中,请稍后再试",
"backup_manual_success": "成功",
"backup_manual_title": "上传状态",
"cache_settings_album_thumbnails": "图库缩略图({} ",
"cache_settings_album_thumbnails": "图库缩略图({} ",
"cache_settings_clear_cache_button": "清除缓存",
"cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。",
"cache_settings_image_cache_size": "图像缓存大小({} ",
"cache_settings_image_cache_size": "图像缓存大小({} ",
"cache_settings_statistics_album": "图库缩略图",
"cache_settings_statistics_assets": "{} {}",
"cache_settings_statistics_assets": "{} {}",
"cache_settings_statistics_full": "完整图像",
"cache_settings_statistics_shared": "共享相册缩略图",
"cache_settings_statistics_thumbnail": "缩略图",
"cache_settings_statistics_title": "缓存使用情况",
"cache_settings_subtitle": "控制 Immich 的缓存行为",
"cache_settings_thumbnail_size": "缩略图缓存大小({} ",
"cache_settings_thumbnail_size": "缩略图缓存大小({} ",
"cache_settings_title": "缓存设置",
"change_password_form_confirm_password": "确认密码",
"change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统或被管理员要求更改密码。\n请在下方输入新密码。",
@@ -139,7 +139,7 @@
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "这些项目将从 Immich 和您的设备中永久删除",
"delete_dialog_alert": "这些项目将从 Immich 和 您的设备 中永久删除",
"delete_dialog_cancel": "取消",
"delete_dialog_ok": "删除",
"delete_dialog_title": "永久删除",
@@ -177,7 +177,7 @@
"login_form_button_text": "登录",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http(s)://你的服务器地址:端口/api",
"login_form_endpoint_url": "服务器终结点地址",
"login_form_endpoint_url": "服务器链接地址",
"login_form_err_http": "请注明 http:// 或 https://",
"login_form_err_invalid_email": "无效的电子邮件",
"login_form_err_invalid_url": "无效的地址",
@@ -270,7 +270,7 @@
"share_add_photos": "添加项目",
"share_add_title": "添加标题",
"share_create_album": "创建相册",
"share_dialog_preparing": "这种准备...",
"share_dialog_preparing": "正在准备...",
"share_invite": "邀请相册共享",
"sharing_page_album": "共享相册",
"sharing_page_description": "创建共享相册以与网络中的人共享照片和视频。",

View File

@@ -1,7 +1,7 @@
{
"add_to_album_bottom_sheet_added": "添加到 {album}",
"add_to_album_bottom_sheet_already_exists": "已在 {album} 中",
"advanced_settings_prefer_remote_subtitle": "在某些设备上,从本地的项目加载缩略图的速度非常慢。\n启用此选项以加载远程\n项目。",
"advanced_settings_prefer_remote_subtitle": "在某些设备上,从本地的项目加载缩略图的速度非常慢。\n启用此选项以加载远程。",
"advanced_settings_prefer_remote_title": "优先远程项目",
"advanced_settings_tile_subtitle": "高级用户设置",
"advanced_settings_tile_title": "高级",
@@ -77,7 +77,7 @@
"backup_controller_page_info": "备份信息",
"backup_controller_page_none_selected": "未选择",
"backup_controller_page_remainder": "剩余",
"backup_controller_page_remainder_sub": "要从所选内容备份的剩余照片和视频",
"backup_controller_page_remainder_sub": "所选数据中尚未备份的数据",
"backup_controller_page_select": "选择",
"backup_controller_page_server_storage": "服务器存储",
"backup_controller_page_start_backup": "开始备份",
@@ -89,26 +89,26 @@
"backup_controller_page_total_sub": "选中相册中的所有不重复的视频和图像",
"backup_controller_page_turn_off": "关闭前台备份",
"backup_controller_page_turn_on": "开启前台备份",
"backup_controller_page_uploading_file_info": "正在上传文件信息",
"backup_controller_page_uploading_file_info": "正在上传中的文件信息",
"backup_err_only_album": "不能移除唯一的一个相册",
"backup_info_card_assets": "",
"backup_manual_cancelled": "Cancelled",
"backup_info_card_assets": "",
"backup_manual_cancelled": "已取消",
"backup_manual_failed": "失败",
"backup_manual_in_progress": "上传正在进行中,请稍后再试",
"backup_manual_success": "成功",
"backup_manual_title": "上传状态",
"cache_settings_album_thumbnails": "图库缩略图({} ",
"cache_settings_album_thumbnails": "图库缩略图({} ",
"cache_settings_clear_cache_button": "清除缓存",
"cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。",
"cache_settings_image_cache_size": "图像缓存大小({} ",
"cache_settings_image_cache_size": "图像缓存大小({} ",
"cache_settings_statistics_album": "图库缩略图",
"cache_settings_statistics_assets": "{} {}",
"cache_settings_statistics_assets": "{} {}",
"cache_settings_statistics_full": "完整图像",
"cache_settings_statistics_shared": "共享相册缩略图",
"cache_settings_statistics_thumbnail": "缩略图",
"cache_settings_statistics_title": "缓存使用情况",
"cache_settings_subtitle": "控制 Immich 的缓存行为",
"cache_settings_thumbnail_size": "缩略图缓存大小({} ",
"cache_settings_thumbnail_size": "缩略图缓存大小({} ",
"cache_settings_title": "缓存设置",
"change_password_form_confirm_password": "确认密码",
"change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统或被管理员要求更改密码。\n请在下方输入新密码。",
@@ -139,7 +139,7 @@
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "这些项目将从 Immich 和您的设备中永久删除",
"delete_dialog_alert": "这些项目将从 Immich 和 您的设备 中永久删除",
"delete_dialog_cancel": "取消",
"delete_dialog_ok": "删除",
"delete_dialog_title": "永久删除",
@@ -177,7 +177,7 @@
"login_form_button_text": "登录",
"login_form_email_hint": "youremail@email.com",
"login_form_endpoint_hint": "http(s)://你的服务器地址:端口/api",
"login_form_endpoint_url": "服务器终结点地址",
"login_form_endpoint_url": "服务器链接地址",
"login_form_err_http": "请注明 http:// 或 https://",
"login_form_err_invalid_email": "无效的电子邮件",
"login_form_err_invalid_url": "无效的地址",
@@ -270,7 +270,7 @@
"share_add_photos": "添加项目",
"share_add_title": "添加标题",
"share_create_album": "创建相册",
"share_dialog_preparing": "这种准备...",
"share_dialog_preparing": "正在准备...",
"share_invite": "邀请相册共享",
"sharing_page_album": "共享相册",
"sharing_page_description": "创建共享相册以与网络中的人共享照片和视频。",

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.77.0"
version_number: "1.78.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -29,6 +29,7 @@ import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:immich_mobile/shared/services/local_notification.service.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:isar/isar.dart';
@@ -41,6 +42,7 @@ void main() async {
final db = await loadDb();
await initApp();
await migrateDatabaseIfNeeded(db);
HttpOverrides.global = HttpSSLCertOverride();
runApp(getMainWidget(db));
}

View File

@@ -7,6 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:latlong2/latlong.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
@@ -17,8 +19,12 @@ class ExifBottomSheet extends HookConsumerWidget {
const ExifBottomSheet({Key? key, required this.asset}) : super(key: key);
bool get hasCoordinates =>
asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null;
bool hasCoordinates(ExifInfo? exifInfo) =>
exifInfo != null &&
exifInfo.latitude != null &&
exifInfo.longitude != null &&
exifInfo.latitude != 0 &&
exifInfo.longitude != 0;
String get formattedDateTime {
final fileCreatedAt = asset.fileCreatedAt.toLocal();
@@ -28,13 +34,13 @@ class ExifBottomSheet extends HookConsumerWidget {
return '$date$time';
}
Future<Uri?> _createCoordinatesUri() async {
if (!hasCoordinates) {
Future<Uri?> _createCoordinatesUri(ExifInfo? exifInfo) async {
if (!hasCoordinates(exifInfo)) {
return null;
}
double latitude = asset.exifInfo!.latitude!;
double longitude = asset.exifInfo!.longitude!;
final double latitude = exifInfo!.latitude!;
final double longitude = exifInfo.longitude!;
const zoomLevel = 16;
@@ -72,7 +78,8 @@ class ExifBottomSheet extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final exifInfo = asset.exifInfo;
final assetWithExif = ref.watch(assetDetailProvider(asset));
final exifInfo = (assetWithExif.value ?? asset).exifInfo;
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var textColor = isDarkTheme ? Colors.white : Colors.black;
@@ -101,7 +108,7 @@ class ExifBottomSheet extends HookConsumerWidget {
),
],
onTap: (tapPosition, latLong) async {
Uri? uri = await _createCoordinatesUri();
Uri? uri = await _createCoordinatesUri(exifInfo);
if (uri == null) {
return;
@@ -124,7 +131,7 @@ class ExifBottomSheet extends HookConsumerWidget {
? formatBytes(a.exifInfo!.fileSize!)
: "";
String text = resolution + fileSize;
return text.isEmpty ? null : Text(text);
return text.isNotEmpty ? text : null;
}
buildDragHeader() {
@@ -143,7 +150,7 @@ class ExifBottomSheet extends HookConsumerWidget {
buildLocation() {
// Guard no lat/lng
if (!hasCoordinates) {
if (!hasCoordinates(exifInfo)) {
return Container();
}
@@ -208,7 +215,58 @@ class ExifBottomSheet extends HookConsumerWidget {
);
}
buildImageProperties() {
// Helper to create the ListTile and avoid repeating code
createImagePropertiesListStyle(title, subtitle) => ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.image,
color: textColor.withAlpha(200),
),
titleAlignment: ListTileTitleAlignment.center,
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
subtitle: subtitle,
);
final imgSizeString = buildSizeText(asset);
if (imgSizeString == null && asset.fileName.isNotEmpty) {
// There is only filename
return createImagePropertiesListStyle(
asset.fileName,
null,
);
} else if (imgSizeString != null && asset.fileName.isNotEmpty) {
// There is both filename and size information
return createImagePropertiesListStyle(
asset.fileName,
Text(imgSizeString),
);
} else if (imgSizeString != null && asset.fileName.isEmpty) {
// There is only size information
return createImagePropertiesListStyle(
imgSizeString,
null,
);
}
}
buildDetail() {
final imgProperties = buildImageProperties();
// There are no details
if (imgProperties == null &&
(exifInfo == null || exifInfo.make == null)) {
return Container();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -223,22 +281,7 @@ class ExifBottomSheet extends HookConsumerWidget {
),
).tr(),
),
ListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
leading: Icon(
Icons.image,
color: textColor.withAlpha(200),
),
title: Text(
asset.fileName,
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
subtitle: buildSizeText(asset),
),
if (imgProperties != null) imgProperties,
if (exifInfo?.make != null)
ListTile(
contentPadding: const EdgeInsets.all(0),
@@ -294,7 +337,7 @@ class ExifBottomSheet extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: hasCoordinates ? 5 : 0,
flex: hasCoordinates(exifInfo) ? 5 : 0,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: buildLocation(),
@@ -324,7 +367,7 @@ class ExifBottomSheet extends HookConsumerWidget {
if (asset.isRemote) DescriptionInput(asset: asset),
const SizedBox(height: 8.0),
buildLocation(),
SizedBox(height: hasCoordinates ? 16.0 : 0.0),
SizedBox(height: hasCoordinates(exifInfo) ? 16.0 : 0.0),
buildDetail(),
const SizedBox(height: 50),
],

View File

@@ -64,9 +64,8 @@ class GalleryViewerPage extends HookConsumerWidget {
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
final currentIndex = useState(initialIndex);
final currentAsset = loadAsset(currentIndex.value);
final watchedAsset = ref.watch(assetDetailProvider(currentAsset));
Asset asset() => watchedAsset.value ?? currentAsset;
Asset asset() => currentAsset;
useEffect(
() {
@@ -194,7 +193,7 @@ class GalleryViewerPage extends HookConsumerWidget {
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: ExifBottomSheet(asset: asset()),
child: ExifBottomSheet(asset: currentAsset),
);
},
);

View File

@@ -459,6 +459,7 @@ class BackgroundService {
notifySingleProgress ? _onProgress : (sent, total) {},
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
_onBackupError,
sortAssets: true,
);
if (!ok && !_cancellationToken!.isCancelled) {
_showErrorNotification(

View File

@@ -202,8 +202,9 @@ class BackupService {
Function(String, String, bool) uploadSuccessCb,
Function(int, int) uploadProgressCb,
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
Function(ErrorUploadAsset) errorCb,
) async {
Function(ErrorUploadAsset) errorCb, {
bool sortAssets = false,
}) async {
if (Platform.isAndroid &&
!(await Permission.accessMediaLocation.status).isGranted) {
// double check that permission is granted here, to guard against
@@ -218,7 +219,22 @@ class BackupService {
bool anyErrors = false;
final List<String> duplicatedAssetIds = [];
for (var entity in assetList) {
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
await PhotoManager.requestPermissionExtend();
List<AssetEntity> assetsToUpload = sortAssets
// Upload images before video assets
// these are further sorted by using their creation date
? assetList.sorted(
(a, b) {
final cmp = a.typeInt - b.typeInt;
if (cmp != 0) return cmp;
return a.createDateTime.compareTo(b.createDateTime);
},
)
: assetList.toList();
for (var entity in assetsToUpload) {
try {
if (entity.type == AssetType.video) {
file = await entity.originFile;
@@ -248,7 +264,8 @@ class BackupService {
req.fields['deviceAssetId'] = entity.id;
req.fields['deviceId'] = deviceId;
req.fields['fileCreatedAt'] = entity.createDateTime.toUtc().toIso8601String();
req.fields['fileCreatedAt'] =
entity.createDateTime.toUtc().toIso8601String();
req.fields['fileModifiedAt'] =
entity.modifiedDateTime.toUtc().toIso8601String();
req.fields['isFavorite'] = entity.isFavorite.toString();

View File

@@ -1,13 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';
class ThumbnailImage extends HookConsumerWidget {
class ThumbnailImage extends StatelessWidget {
final Asset asset;
final int index;
final Asset Function(int index) loadAsset;
@@ -36,7 +35,7 @@ class ThumbnailImage extends HookConsumerWidget {
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final assetContainerColor =
isDarkTheme ? Colors.blueGrey : Theme.of(context).primaryColorLight;
@@ -61,8 +60,8 @@ class ThumbnailImage extends HookConsumerWidget {
}
}
Widget buildImage(Asset asset) {
var image = SizedBox(
Widget buildImage() {
final image = SizedBox(
width: 300,
height: 300,
child: Hero(
@@ -133,7 +132,7 @@ class ThumbnailImage extends HookConsumerWidget {
)
: const Border(),
),
child: buildImage(asset),
child: buildImage(),
),
if (multiselectEnabled)
Padding(

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -88,6 +89,16 @@ class LoginForm extends HookConsumerWidget {
isPasswordLoginEnable.value = true;
isLoadingServer.value = false;
return false;
} on HandshakeException {
ImmichToast.show(
context: context,
msg: 'login_form_handshake_exception'.tr(),
toastType: ToastType.error,
);
isOauthEnable.value = false;
isPasswordLoginEnable.value = true;
isLoadingServer.value = false;
return false;
} catch (e) {
ImmichToast.show(
context: context,
@@ -226,6 +237,7 @@ class LoginForm extends HookConsumerWidget {
}
buildSelectServer() {
const buttonRadius = 25.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -235,24 +247,51 @@ class LoginForm extends HookConsumerWidget {
onSubmit: getServerLoginCredential,
),
const SizedBox(height: 18),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: isLoadingServer.value ? null : getServerLoginCredential,
icon: const Icon(Icons.arrow_forward_rounded),
label: const Text(
'login_form_next_button',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
),
if (isLoadingServer.value)
const Padding(
padding: EdgeInsets.only(top: 18.0),
child: Center(
child: CircularProgressIndicator(),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(buttonRadius),
bottomLeft: Radius.circular(buttonRadius),
),
),
),
onPressed: () =>
AutoRouter.of(context).push(const SettingsRoute()),
icon: const Icon(Icons.settings_rounded),
label: const SizedBox.shrink(),
),
),
),
const SizedBox(width: 1),
Expanded(
flex: 3,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(buttonRadius),
bottomRight: Radius.circular(buttonRadius),
),
),
),
onPressed:
isLoadingServer.value ? null : getServerLoginCredential,
icon: const Icon(Icons.arrow_forward_rounded),
label: const Text(
'login_form_next_button',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
),
),
],
),
const SizedBox(height: 18),
if (isLoadingServer.value) const LoadingIcon(),
],
);
}
@@ -285,18 +324,7 @@ class LoginForm extends HookConsumerWidget {
// Note: This used to have an AnimatedSwitcher, but was removed
// because of https://github.com/flutter/flutter/issues/120874
isLoading.value
? const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
)
? const LoadingIcon()
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
@@ -572,3 +600,23 @@ class OAuthLoginButton extends ConsumerWidget {
);
}
}
class LoadingIcon extends StatelessWidget {
const LoadingIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/user.dart';
@@ -14,6 +15,14 @@ class PartnerDetailPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final assets = ref.watch(assetsProvider(partner.isarId));
useEffect(
() {
ref.read(assetProvider.notifier).getPartnerAssets(partner);
return null;
},
[],
);
return Scaffold(
appBar: AppBar(
title: Text("${partner.firstName} ${partner.lastName}"),
@@ -30,7 +39,8 @@ class PartnerDetailPage extends HookConsumerWidget {
)
: ImmichAssetGrid(
renderList: renderList,
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
onRefresh: () =>
ref.read(assetProvider.notifier).getPartnerAssets(partner),
),
error: (e, _) => Text("Error loading partners:\n$e"),
loading: () => const Center(child: ImmichLoadingIndicator()),

View File

@@ -49,6 +49,7 @@ enum AppSettingsEnum<T> {
mapThemeMode<bool>(StoreKey.mapThemeMode, null, false),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
;
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

View File

@@ -1,23 +1,29 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState;
import 'package:immich_mobile/shared/models/store.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
import 'package:logging/logging.dart';
class AdvancedSettings extends HookConsumerWidget {
const AdvancedSettings({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
bool isLoggedIn = Store.tryGet(StoreKey.currentUser) != null;
final appSettingService = ref.watch(appSettingsServiceProvider);
final isEnabled =
useState(AppSettingsEnum.advancedTroubleshooting.defaultValue);
final levelId = useState(AppSettingsEnum.logLevel.defaultValue);
final preferRemote =
useState(AppSettingsEnum.preferRemoteImage.defaultValue);
final allowSelfSignedSSLCert =
useState(AppSettingsEnum.allowSelfSignedSSLCert.defaultValue);
useEffect(
() {
@@ -27,6 +33,8 @@ class AdvancedSettings extends HookConsumerWidget {
levelId.value = appSettingService.getSetting(AppSettingsEnum.logLevel);
preferRemote.value =
appSettingService.getSetting(AppSettingsEnum.preferRemoteImage);
allowSelfSignedSSLCert.value = appSettingService
.getSetting(AppSettingsEnum.allowSelfSignedSSLCert);
return null;
},
[],
@@ -88,6 +96,17 @@ class AdvancedSettings extends HookConsumerWidget {
title: "advanced_settings_prefer_remote_title".tr(),
subtitle: "advanced_settings_prefer_remote_subtitle".tr(),
),
SettingsSwitchListTile(
enabled: !isLoggedIn,
appSettingService: appSettingService,
valueNotifier: allowSelfSignedSSLCert,
settingsEnum: AppSettingsEnum.allowSelfSignedSSLCert,
title: "advanced_settings_self_signed_ssl_title".tr(),
subtitle: "advanced_settings_self_signed_ssl_subtitle".tr(),
onChanged: (value) {
HttpOverrides.global = HttpSSLCertOverride();
},
),
],
);
}

View File

@@ -8,6 +8,7 @@ class SettingsSwitchListTile extends StatelessWidget {
final String title;
final bool enabled;
final String? subtitle;
final Function(bool)? onChanged;
SettingsSwitchListTile({
required this.appSettingService,
@@ -16,19 +17,26 @@ class SettingsSwitchListTile extends StatelessWidget {
required this.title,
this.subtitle,
this.enabled = true,
this.onChanged,
}) : super(key: Key(settingsEnum.name));
@override
Widget build(BuildContext context) {
return SwitchListTile.adaptive(
selectedTileColor: enabled ? null : Theme.of(context).disabledColor,
value: valueNotifier.value,
onChanged: !enabled
? null
: (value) {
valueNotifier.value = value;
appSettingService.setSetting(settingsEnum, value);
},
activeColor: Theme.of(context).primaryColor,
onChanged: (bool value) {
if (enabled) {
valueNotifier.value = value;
appSettingService.setSetting(settingsEnum, value);
}
if (onChanged != null) {
onChanged!(value);
}
},
activeColor: enabled
? Theme.of(context).primaryColor
: Theme.of(context).disabledColor,
dense: true,
title: Text(
title,

View File

@@ -125,7 +125,6 @@ part 'router.gr.dart';
AutoRoute(
page: SettingsPage,
guards: [
AuthGuard,
DuplicateGuard,
],
),

View File

@@ -550,10 +550,7 @@ class _$AppRouter extends RootStackRouter {
RouteConfig(
SettingsRoute.name,
path: '/settings-page',
guards: [
authGuard,
duplicateGuard,
],
guards: [duplicateGuard],
),
RouteConfig(
AppLogRoute.name,
@@ -1351,6 +1348,7 @@ class MemoryRouteArgs {
}
}
/// generated route for
/// [MapPage]
class MapRoute extends PageRouteInfo<void> {
const MapRoute()

View File

@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
class TabNavigationObserver extends AutoRouterObserver {
@@ -42,6 +43,7 @@ class TabNavigationObserver extends AutoRouterObserver {
if (route.name == 'SharingRoute') {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.read(assetProvider.notifier).getPartnerAssets();
}
if (route.name == 'LibraryRoute') {
@@ -50,6 +52,7 @@ class TabNavigationObserver extends AutoRouterObserver {
if (route.name == 'HomeRoute') {
ref.invalidate(memoryFutureProvider);
Future(() => ref.read(assetProvider.notifier).getAllAsset());
// Update user info
try {

View File

@@ -417,17 +417,17 @@ enum AssetState {
extension AssetsHelper on IsarCollection<Asset> {
Future<int> deleteAllByRemoteId(Iterable<String> ids) =>
ids.isEmpty ? Future.value(0) : _remote(ids).deleteAll();
ids.isEmpty ? Future.value(0) : remote(ids).deleteAll();
Future<int> deleteAllByLocalId(Iterable<String> ids) =>
ids.isEmpty ? Future.value(0) : _local(ids).deleteAll();
ids.isEmpty ? Future.value(0) : local(ids).deleteAll();
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
ids.isEmpty ? Future.value([]) : _remote(ids).findAll();
ids.isEmpty ? Future.value([]) : remote(ids).findAll();
Future<List<Asset>> getAllByLocalId(Iterable<String> ids) =>
ids.isEmpty ? Future.value([]) : _local(ids).findAll();
ids.isEmpty ? Future.value([]) : local(ids).findAll();
QueryBuilder<Asset, Asset, QAfterWhereClause> _remote(Iterable<String> ids) =>
QueryBuilder<Asset, Asset, QAfterWhereClause> remote(Iterable<String> ids) =>
where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e));
QueryBuilder<Asset, Asset, QAfterWhereClause> _local(Iterable<String> ids) {
QueryBuilder<Asset, Asset, QAfterWhereClause> local(Iterable<String> ids) {
return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e));
}
}

View File

@@ -5,9 +5,10 @@ part 'etag.g.dart';
@Collection(inheritance: false)
class ETag {
ETag({required this.id, this.value});
ETag({required this.id, this.assetCount, this.time});
Id get isarId => fastHash(id);
@Index(unique: true, replace: true, type: IndexType.hash)
String id;
String? value;
int? assetCount;
DateTime? time;
}

View File

@@ -17,15 +17,20 @@ const ETagSchema = CollectionSchema(
name: r'ETag',
id: -644290296585643859,
properties: {
r'id': PropertySchema(
r'assetCount': PropertySchema(
id: 0,
name: r'assetCount',
type: IsarType.long,
),
r'id': PropertySchema(
id: 1,
name: r'id',
type: IsarType.string,
),
r'value': PropertySchema(
id: 1,
name: r'value',
type: IsarType.string,
r'time': PropertySchema(
id: 2,
name: r'time',
type: IsarType.dateTime,
)
},
estimateSize: _eTagEstimateSize,
@@ -63,12 +68,6 @@ int _eTagEstimateSize(
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.id.length * 3;
{
final value = object.value;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
@@ -78,8 +77,9 @@ void _eTagSerialize(
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.id);
writer.writeString(offsets[1], object.value);
writer.writeLong(offsets[0], object.assetCount);
writer.writeString(offsets[1], object.id);
writer.writeDateTime(offsets[2], object.time);
}
ETag _eTagDeserialize(
@@ -89,8 +89,9 @@ ETag _eTagDeserialize(
Map<Type, List<int>> allOffsets,
) {
final object = ETag(
id: reader.readString(offsets[0]),
value: reader.readStringOrNull(offsets[1]),
assetCount: reader.readLongOrNull(offsets[0]),
id: reader.readString(offsets[1]),
time: reader.readDateTimeOrNull(offsets[2]),
);
return object;
}
@@ -103,9 +104,11 @@ P _eTagDeserializeProp<P>(
) {
switch (propertyId) {
case 0:
return (reader.readString(offset)) as P;
return (reader.readLongOrNull(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
return (reader.readString(offset)) as P;
case 2:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
@@ -294,6 +297,75 @@ extension ETagQueryWhere on QueryBuilder<ETag, ETag, QWhereClause> {
}
extension ETagQueryFilter on QueryBuilder<ETag, ETag, QFilterCondition> {
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'assetCount',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'assetCount',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountEqualTo(
int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'assetCount',
value: value,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'assetCount',
value: value,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'assetCount',
value: value,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'assetCount',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idEqualTo(
String value, {
bool caseSensitive = true,
@@ -474,146 +546,70 @@ extension ETagQueryFilter on QueryBuilder<ETag, ETag, QFilterCondition> {
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNull() {
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'value',
property: r'time',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNotNull() {
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'value',
property: r'time',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueEqualTo(
String? value, {
bool caseSensitive = true,
}) {
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeEqualTo(DateTime? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'value',
property: r'time',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueGreaterThan(
String? value, {
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeGreaterThan(
DateTime? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'value',
property: r'time',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueLessThan(
String? value, {
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeLessThan(
DateTime? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'value',
property: r'time',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueBetween(
String? lower,
String? upper, {
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'value',
property: r'time',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'value',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'value',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'value',
value: '',
));
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> valueIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'value',
value: '',
));
});
}
@@ -624,6 +620,18 @@ extension ETagQueryObject on QueryBuilder<ETag, ETag, QFilterCondition> {}
extension ETagQueryLinks on QueryBuilder<ETag, ETag, QFilterCondition> {}
extension ETagQuerySortBy on QueryBuilder<ETag, ETag, QSortBy> {
QueryBuilder<ETag, ETag, QAfterSortBy> sortByAssetCount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByAssetCountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@@ -636,20 +644,32 @@ extension ETagQuerySortBy on QueryBuilder<ETag, ETag, QSortBy> {
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByValue() {
QueryBuilder<ETag, ETag, QAfterSortBy> sortByTime() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.asc);
return query.addSortBy(r'time', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByValueDesc() {
QueryBuilder<ETag, ETag, QAfterSortBy> sortByTimeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.desc);
return query.addSortBy(r'time', Sort.desc);
});
}
}
extension ETagQuerySortThenBy on QueryBuilder<ETag, ETag, QSortThenBy> {
QueryBuilder<ETag, ETag, QAfterSortBy> thenByAssetCount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByAssetCountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@@ -674,20 +694,26 @@ extension ETagQuerySortThenBy on QueryBuilder<ETag, ETag, QSortThenBy> {
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByValue() {
QueryBuilder<ETag, ETag, QAfterSortBy> thenByTime() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.asc);
return query.addSortBy(r'time', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByValueDesc() {
QueryBuilder<ETag, ETag, QAfterSortBy> thenByTimeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'value', Sort.desc);
return query.addSortBy(r'time', Sort.desc);
});
}
}
extension ETagQueryWhereDistinct on QueryBuilder<ETag, ETag, QDistinct> {
QueryBuilder<ETag, ETag, QDistinct> distinctByAssetCount() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'assetCount');
});
}
QueryBuilder<ETag, ETag, QDistinct> distinctById(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@@ -695,10 +721,9 @@ extension ETagQueryWhereDistinct on QueryBuilder<ETag, ETag, QDistinct> {
});
}
QueryBuilder<ETag, ETag, QDistinct> distinctByValue(
{bool caseSensitive = true}) {
QueryBuilder<ETag, ETag, QDistinct> distinctByTime() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'value', caseSensitive: caseSensitive);
return query.addDistinctBy(r'time');
});
}
}
@@ -710,15 +735,21 @@ extension ETagQueryProperty on QueryBuilder<ETag, ETag, QQueryProperty> {
});
}
QueryBuilder<ETag, int?, QQueryOperations> assetCountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'assetCount');
});
}
QueryBuilder<ETag, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<ETag, String?, QQueryOperations> valueProperty() {
QueryBuilder<ETag, DateTime?, QQueryOperations> timeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'value');
return query.addPropertyName(r'time');
});
}
}

View File

@@ -178,6 +178,7 @@ enum StoreKey<T> {
mapThemeMode<bool>(117, type: bool),
mapShowFavoriteOnly<bool>(118, type: bool),
mapRelativeDate<int>(119, type: int),
selfSignedCert<bool>(120, type: bool),
;
const StoreKey(

View File

@@ -1,4 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
@@ -11,6 +13,7 @@ import 'package:immich_mobile/modules/settings/providers/notification_permission
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/tab.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -47,8 +50,18 @@ class AppStateNotiifer extends StateNotifier<AppStateEnum> {
if (isAuthenticated && (permission.isGranted || permission.isLimited)) {
ref.read(backupProvider.notifier).resumeBackup();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
ref.watch(assetProvider.notifier).getAllAsset();
ref.watch(serverInfoProvider.notifier).getServerVersion();
ref.read(serverInfoProvider.notifier).getServerVersion();
switch (ref.read(tabProvider)) {
case TabEnum.home:
ref.read(assetProvider.notifier).getAllAsset();
case TabEnum.search:
// nothing to do
case TabEnum.sharing:
ref.read(assetProvider.notifier).getPartnerAssets();
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
case TabEnum.library:
ref.read(albumProvider.notifier).getAllAlbums();
}
}
ref.watch(websocketProvider.notifier).connect();

View File

@@ -27,6 +27,7 @@ class AssetNotifier extends StateNotifier<bool> {
final log = Logger('AssetNotifier');
bool _getAllAssetInProgress = false;
bool _deleteInProgress = false;
bool _getPartnerAssetsInProgress = false;
AssetNotifier(
this._assetService,
@@ -49,15 +50,10 @@ class AssetNotifier extends StateNotifier<bool> {
await clearAssetsAndAlbums(_db);
log.info("Manual refresh requested, cleared assets and albums from db");
}
await _userService.refreshUsers();
final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
final List<User> partners =
await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll();
for (User u in partners) {
await _assetService.refreshRemoteAssets(u);
}
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
} finally {
_getAllAssetInProgress = false;
@@ -65,6 +61,27 @@ class AssetNotifier extends StateNotifier<bool> {
}
}
Future<void> getPartnerAssets([User? partner]) async {
if (_getPartnerAssetsInProgress) return;
try {
final stopwatch = Stopwatch()..start();
_getPartnerAssetsInProgress = true;
if (partner == null) {
await _userService.refreshUsers();
final List<User> partners =
await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll();
for (User u in partners) {
await _assetService.refreshRemoteAssets(u);
}
} else {
await _assetService.refreshRemoteAssets(partner);
}
log.info("Load partner assets: ${stopwatch.elapsedMilliseconds}ms");
} finally {
_getPartnerAssetsInProgress = false;
}
}
Future<void> clearAllAsset() {
return clearAssetsAndAlbums(_db);
}

View File

@@ -0,0 +1,13 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
enum TabEnum {
home,
search,
sharing,
library,
}
/// Provides the currently active tab
final tabProvider = StateProvider<TabEnum>(
(ref) => TabEnum.home,
);

View File

@@ -20,6 +20,7 @@ class ApiService {
late ServerInfoApi serverInfoApi;
late PartnerApi partnerApi;
late PersonApi personApi;
late AuditApi auditApi;
ApiService() {
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
@@ -43,6 +44,7 @@ class ApiService {
searchApi = SearchApi(_apiClient);
partnerApi = PartnerApi(_apiClient);
personApi = PersonApi(_apiClient);
auditApi = AuditApi(_apiClient);
}
Future<String> resolveAndSetEndpoint(String serverUrl) async {

View File

@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
@@ -11,7 +10,6 @@ import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/openapi_extensions.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -39,37 +37,34 @@ class AssetService {
/// Checks the server for updated assets and updates the local database if
/// required. Returns `true` if there were any changes.
Future<bool> refreshRemoteAssets([User? user]) async {
user ??= Store.get(StoreKey.currentUser);
user ??= Store.get<User>(StoreKey.currentUser);
final Stopwatch sw = Stopwatch()..start();
final int numOwnedRemoteAssets = await _db.assets
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(user!.isarId)
.count();
final bool changes = await _syncService.syncRemoteAssetsToDb(
user,
() async => (await _getRemoteAssets(
hasCache: numOwnedRemoteAssets > 0,
user: user!,
)),
_getRemoteAssetChanges,
_getRemoteAssets,
);
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes;
}
/// Returns `(null, null)` if changes are invalid -> requires full sync
Future<(List<Asset>? toUpsert, List<String>? toDelete)>
_getRemoteAssetChanges(User user, DateTime since) async {
final deleted = await _apiService.auditApi
.getAuditDeletes(EntityType.ASSET, since, userId: user.id);
if (deleted == null || deleted.needsFullSync) return (null, null);
final assetDto = await _apiService.assetApi
.getAllAssets(userId: user.id, updatedAfter: since);
if (assetDto == null) return (null, null);
return (assetDto.map(Asset.remote).toList(), deleted.ids);
}
/// Returns `null` if the server state did not change, else list of assets
Future<List<Asset>?> _getRemoteAssets({
required bool hasCache,
required User user,
}) async {
Future<List<Asset>?> _getRemoteAssets(User user) async {
try {
final etag = hasCache ? _db.eTags.getByIdSync(user.id)?.value : null;
final (List<AssetResponseDto>? assets, String? newETag) =
await _apiService.assetApi.getAllAssetsWithETag(
eTag: etag,
userId: user.id,
);
final List<AssetResponseDto>? assets =
await _apiService.assetApi.getAllAssets(userId: user.id);
if (assets == null) {
return null;
} else if (assets.isNotEmpty && assets.first.ownerId != user.id) {
@@ -77,8 +72,6 @@ class AssetService {
" The server returned assets for user ${assets.first.ownerId}"
" while requesting assets of user ${user.id}");
return null;
} else if (newETag != etag) {
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, value: newETag)));
}
return assets.map(Asset.remote).toList();
} catch (error, stack) {

View File

@@ -69,9 +69,17 @@ class SyncService {
/// Returns `true` if there were any changes
Future<bool> syncRemoteAssetsToDb(
User user,
FutureOr<List<Asset>?> Function() loadAssets,
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
User user,
DateTime since,
) getChangedAssets,
FutureOr<List<Asset>?> Function(User user) loadAssets,
) =>
_lock.run(() => _syncRemoteAssetsToDb(user, loadAssets));
_lock.run(
() async =>
await _syncRemoteAssetChanges(user, getChangedAssets) ??
await _syncRemoteAssetsFull(user, loadAssets),
);
/// Syncs remote albums to the database
/// returns `true` if there were any changes
@@ -130,13 +138,59 @@ class SyncService {
return true;
}
/// Syncs remote assets to the databas
/// returns `true` if there were any changes
Future<bool> _syncRemoteAssetsToDb(
/// Efficiently syncs assets via changes. Returns `null` when a full sync is required.
Future<bool?> _syncRemoteAssetChanges(
User user,
FutureOr<List<Asset>?> Function() loadAssets,
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
User user,
DateTime since,
) getChangedAssets,
) async {
final List<Asset>? remote = await loadAssets();
final DateTime? since = _db.eTags.getByIdSync(user.id)?.time?.toUtc();
if (since == null) return null;
final DateTime now = DateTime.now();
final (toUpsert, toDelete) = await getChangedAssets(user, since);
if (toUpsert == null || toDelete == null) return null;
try {
if (toDelete.isNotEmpty) {
await _handleRemoteAssetRemoval(toDelete);
}
if (toUpsert.isNotEmpty) {
final (_, updated) = await _linkWithExistingFromDb(toUpsert);
await upsertAssetsWithExif(updated);
}
if (toUpsert.isNotEmpty || toDelete.isNotEmpty) {
await _updateUserAssetsETag(user, now);
return true;
}
return false;
} on IsarError catch (e) {
_log.severe("Failed to sync remote assets to db: $e");
}
return null;
}
/// Deletes remote-only assets, updates merged assets to be local-only
Future<void> _handleRemoteAssetRemoval(List<String> idsToDelete) {
return _db.writeTxn(() async {
await _db.assets.remote(idsToDelete).filter().localIdIsNull().deleteAll();
final onlyLocal = await _db.assets.remote(idsToDelete).findAll();
if (onlyLocal.isNotEmpty) {
for (final Asset a in onlyLocal) {
a.remoteId = null;
}
await _db.assets.putAll(onlyLocal);
}
});
}
/// Syncs assets by loading and comparing all assets from the server.
Future<bool> _syncRemoteAssetsFull(
User user,
FutureOr<List<Asset>?> Function(User user) loadAssets,
) async {
final DateTime now = DateTime.now();
final List<Asset>? remote = await loadAssets(user);
if (remote == null) {
return false;
}
@@ -150,6 +204,7 @@ class SyncService {
remote.sort(Asset.compareByChecksum);
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
await _updateUserAssetsETag(user, now);
return false;
}
final idsToDelete = toRemove.map((e) => e.id).toList();
@@ -159,9 +214,13 @@ class SyncService {
} on IsarError catch (e) {
_log.severe("Failed to sync remote assets to db: $e");
}
await _updateUserAssetsETag(user, now);
return true;
}
Future<void> _updateUserAssetsETag(User user, DateTime time) =>
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, time: time)));
/// Syncs remote albums to the database
/// returns `true` if there were any changes
Future<bool> _syncRemoteAlbumsToDb(
@@ -450,6 +509,14 @@ class SyncService {
_log.fine(
"Only excluded assets in local album ${ape.name} changed. Stopping sync.",
);
if (assetCountOnDevice !=
_db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) {
await _db.writeTxn(
() => _db.eTags.put(
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
),
);
}
return false;
}
_log.fine(
@@ -477,7 +544,7 @@ class SyncService {
album.thumbnail.value ??= await album.assets.filter().findFirst();
await album.thumbnail.save();
await _db.eTags.put(
ETag(id: ape.eTagKeyAssetCount, value: assetCountOnDevice.toString()),
ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice),
);
});
_log.info("Synced changes of local album ${ape.name} to DB");
@@ -496,7 +563,7 @@ class SyncService {
}
final int totalOnDevice = await ape.assetCountAsync;
final int lastKnownTotal =
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.value?.toInt() ?? 0;
(await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0;
final AssetPathEntity? modified = totalOnDevice > lastKnownTotal
? await ape.fetchPathProperties(
filterOptionGroup: FilterOptionGroup(
@@ -523,9 +590,8 @@ class SyncService {
await _db.assets.putAll(updated);
await album.assets.update(link: existingInDb + updated);
await _db.albums.put(album);
await _db.eTags.put(
ETag(id: ape.eTagKeyAssetCount, value: totalOnDevice.toString()),
);
await _db.eTags
.put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice));
});
_log.info("Fast synced local album ${ape.name} to DB");
} on IsarError catch (e) {
@@ -667,7 +733,7 @@ class SyncService {
a.lastModified == null ||
!a.lastModified!.isAtSameMomentAs(b.modifiedAt) ||
await a.assetCountAsync !=
(await _db.eTags.getById(a.eTagKeyAssetCount))?.value?.toInt();
(await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
}
}

View File

@@ -148,7 +148,7 @@ class AppLogPage extends HookConsumerWidget {
),
),
TextSpan(
text: logMessage.message,
text: truncateLogMessage(logMessage.message, 4),
style: const TextStyle(
fontSize: 14.0,
),
@@ -170,4 +170,14 @@ class AppLogPage extends HookConsumerWidget {
),
);
}
/// Truncate the log message to a certain number of lines
/// @param int maxLines - Max number of lines to truncate
String truncateLogMessage(String message, int maxLines) {
List<String> messageLines = message.split("\n");
if (messageLines.length < maxLines) {
return message;
}
return "${messageLines.sublist(0, maxLines).join("\n")} ...";
}
}

View File

@@ -7,6 +7,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.pro
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/tab.provider.dart';
class TabControllerPage extends HookConsumerWidget {
const TabControllerPage({Key? key}) : super(key: key);
@@ -51,6 +52,7 @@ class TabControllerPage extends HookConsumerWidget {
}
HapticFeedback.selectionClick();
tabsRouter.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index];
},
selectedIconTheme: IconThemeData(
color: Theme.of(context).primaryColor,
@@ -103,6 +105,7 @@ class TabControllerPage extends HookConsumerWidget {
}
HapticFeedback.selectionClick();
tabsRouter.setActiveIndex(index);
ref.read(tabProvider.notifier).state = TabEnum.values[index];
},
destinations: [
NavigationDestination(

View File

@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:logging/logging.dart';
class HttpSSLCertOverride extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (X509Certificate cert, String host, int port) {
var log = Logger("HttpSSLCertOverride");
AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert;
// Check if user has allowed self signed SSL certificates.
bool selfSignedCertsAllowed =
Store.get(setting.storeKey as StoreKey<bool>, setting.defaultValue);
bool isLoggedIn = Store.tryGet(StoreKey.currentUser) != null;
// Conduct server host checks if user is logged in to avoid making
// insecure SSL connections to services that are not the immich server.
if (isLoggedIn && selfSignedCertsAllowed) {
String serverHost =
Uri.parse(Store.tryGet(StoreKey.serverEndpoint) ?? "").host;
selfSignedCertsAllowed &= serverHost.contains(host);
}
if (!selfSignedCertsAllowed) {
log.severe("Invalid SSL certificate for $host:$port");
}
return selfSignedCertsAllowed;
};
}
}

View File

@@ -1,59 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:openapi/api.dart';
/// Extension methods to retrieve ETag together with the API call
extension WithETag on AssetApi {
/// Get all AssetEntity belong to the user
///
/// Parameters:
///
/// * [String] eTag:
/// ETag of data already cached on the client
Future<(List<AssetResponseDto>? assets, String? eTag)> getAllAssetsWithETag({
String? eTag,
String? userId,
bool? isFavorite,
bool? isArchived,
}) async {
final response = await getAllAssetsWithHttpInfo(
ifNoneMatch: eTag,
userId: userId,
isFavorite: isFavorite,
isArchived: isArchived,
);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty &&
response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
final etag = response.headers[HttpHeaders.etagHeader];
final data = (await apiClient.deserializeAsync(
responseBody,
'List<AssetResponseDto>',
) as List)
.cast<AssetResponseDto>()
.toList();
return (data, etag);
}
return (null, null);
}
}
/// Returns the decoded body as UTF-8 if the given headers indicate an 'application/json'
/// content type. Otherwise, returns the decoded body as decoded by dart:http package.
Future<String> _decodeBodyBytes(Response response) async {
final contentType = response.headers['content-type'];
return contentType != null &&
contentType.toLowerCase().startsWith('application/json')
? response.bodyBytes.isEmpty
? ''
: utf8.decode(response.bodyBytes)
: response.body;
}

View File

@@ -97,6 +97,7 @@ doc/SearchExploreResponseDto.md
doc/SearchFacetCountResponseDto.md
doc/SearchFacetResponseDto.md
doc/SearchResponseDto.md
doc/ServerConfigDto.md
doc/ServerFeaturesDto.md
doc/ServerInfoApi.md
doc/ServerInfoResponseDto.md
@@ -116,6 +117,7 @@ doc/SystemConfigDto.md
doc/SystemConfigFFmpegDto.md
doc/SystemConfigJobDto.md
doc/SystemConfigMachineLearningDto.md
doc/SystemConfigMapDto.md
doc/SystemConfigOAuthDto.md
doc/SystemConfigPasswordLoginDto.md
doc/SystemConfigStorageTemplateDto.md
@@ -249,6 +251,7 @@ lib/model/search_explore_response_dto.dart
lib/model/search_facet_count_response_dto.dart
lib/model/search_facet_response_dto.dart
lib/model/search_response_dto.dart
lib/model/server_config_dto.dart
lib/model/server_features_dto.dart
lib/model/server_info_response_dto.dart
lib/model/server_media_types_response_dto.dart
@@ -265,6 +268,7 @@ lib/model/system_config_dto.dart
lib/model/system_config_f_fmpeg_dto.dart
lib/model/system_config_job_dto.dart
lib/model/system_config_machine_learning_dto.dart
lib/model/system_config_map_dto.dart
lib/model/system_config_o_auth_dto.dart
lib/model/system_config_password_login_dto.dart
lib/model/system_config_storage_template_dto.dart
@@ -382,6 +386,7 @@ test/search_explore_response_dto_test.dart
test/search_facet_count_response_dto_test.dart
test/search_facet_response_dto_test.dart
test/search_response_dto_test.dart
test/server_config_dto_test.dart
test/server_features_dto_test.dart
test/server_info_api_test.dart
test/server_info_response_dto_test.dart
@@ -401,6 +406,7 @@ test/system_config_dto_test.dart
test/system_config_f_fmpeg_dto_test.dart
test/system_config_job_dto_test.dart
test/system_config_machine_learning_dto_test.dart
test/system_config_map_dto_test.dart
test/system_config_o_auth_dto_test.dart
test/system_config_password_login_dto_test.dart
test/system_config_storage_template_dto_test.dart

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.77.0
- API version: 1.78.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -142,6 +142,7 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
@@ -266,6 +267,7 @@ Class | Method | HTTP request | Description
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
- [SearchResponseDto](doc//SearchResponseDto.md)
- [ServerConfigDto](doc//ServerConfigDto.md)
- [ServerFeaturesDto](doc//ServerFeaturesDto.md)
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
- [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md)
@@ -282,6 +284,7 @@ Class | Method | HTTP request | Description
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)

17
mobile/openapi/doc/ServerConfigDto.md generated Normal file
View File

@@ -0,0 +1,17 @@
# openapi.model.ServerConfigDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**loginPageMessage** | **String** | |
**mapTileUrl** | **String** | |
**oauthButtonText** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -11,6 +11,7 @@ Name | Type | Description | Notes
**clipEncode** | **bool** | |
**configFile** | **bool** | |
**facialRecognition** | **bool** | |
**map** | **bool** | |
**oauth** | **bool** | |
**oauthAutoLaunch** | **bool** | |
**passwordLogin** | **bool** | |

View File

@@ -9,6 +9,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getServerConfig**](ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
[**getServerFeatures**](ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info |
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
@@ -17,6 +18,43 @@ Method | HTTP request | Description
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
# **getServerConfig**
> ServerConfigDto getServerConfig()
### Example
```dart
import 'package:openapi/api.dart';
final api_instance = ServerInfoApi();
try {
final result = api_instance.getServerConfig();
print(result);
} catch (e) {
print('Exception when calling ServerInfoApi->getServerConfig: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**ServerConfigDto**](ServerConfigDto.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getServerFeatures**
> ServerFeaturesDto getServerFeatures()

View File

@@ -11,6 +11,7 @@ Name | Type | Description | Notes
**ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | |
**job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | |
**machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | |
**map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) | |
**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) | |
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | |
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | |

16
mobile/openapi/doc/SystemConfigMapDto.md generated Normal file
View File

@@ -0,0 +1,16 @@
# openapi.model.SystemConfigMapDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**enabled** | **bool** | |
**tileUrl** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -128,6 +128,7 @@ part 'model/search_explore_response_dto.dart';
part 'model/search_facet_count_response_dto.dart';
part 'model/search_facet_response_dto.dart';
part 'model/search_response_dto.dart';
part 'model/server_config_dto.dart';
part 'model/server_features_dto.dart';
part 'model/server_info_response_dto.dart';
part 'model/server_media_types_response_dto.dart';
@@ -144,6 +145,7 @@ part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_job_dto.dart';
part 'model/system_config_machine_learning_dto.dart';
part 'model/system_config_map_dto.dart';
part 'model/system_config_o_auth_dto.dart';
part 'model/system_config_password_login_dto.dart';
part 'model/system_config_storage_template_dto.dart';

View File

@@ -16,6 +16,47 @@ class ServerInfoApi {
final ApiClient apiClient;
/// Performs an HTTP 'GET /server-info/config' operation and returns the [Response].
Future<Response> getServerConfigWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server-info/config';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<ServerConfigDto?> getServerConfig() async {
final response = await getServerConfigWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerConfigDto',) as ServerConfigDto;
}
return null;
}
/// Performs an HTTP 'GET /server-info/features' operation and returns the [Response].
Future<Response> getServerFeaturesWithHttpInfo() async {
// ignore: prefer_const_declarations

View File

@@ -349,6 +349,8 @@ class ApiClient {
return SearchFacetResponseDto.fromJson(value);
case 'SearchResponseDto':
return SearchResponseDto.fromJson(value);
case 'ServerConfigDto':
return ServerConfigDto.fromJson(value);
case 'ServerFeaturesDto':
return ServerFeaturesDto.fromJson(value);
case 'ServerInfoResponseDto':
@@ -381,6 +383,8 @@ class ApiClient {
return SystemConfigJobDto.fromJson(value);
case 'SystemConfigMachineLearningDto':
return SystemConfigMachineLearningDto.fromJson(value);
case 'SystemConfigMapDto':
return SystemConfigMapDto.fromJson(value);
case 'SystemConfigOAuthDto':
return SystemConfigOAuthDto.fromJson(value);
case 'SystemConfigPasswordLoginDto':

View File

@@ -0,0 +1,114 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ServerConfigDto {
/// Returns a new [ServerConfigDto] instance.
ServerConfigDto({
required this.loginPageMessage,
required this.mapTileUrl,
required this.oauthButtonText,
});
String loginPageMessage;
String mapTileUrl;
String oauthButtonText;
@override
bool operator ==(Object other) => identical(this, other) || other is ServerConfigDto &&
other.loginPageMessage == loginPageMessage &&
other.mapTileUrl == mapTileUrl &&
other.oauthButtonText == oauthButtonText;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(loginPageMessage.hashCode) +
(mapTileUrl.hashCode) +
(oauthButtonText.hashCode);
@override
String toString() => 'ServerConfigDto[loginPageMessage=$loginPageMessage, mapTileUrl=$mapTileUrl, oauthButtonText=$oauthButtonText]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'loginPageMessage'] = this.loginPageMessage;
json[r'mapTileUrl'] = this.mapTileUrl;
json[r'oauthButtonText'] = this.oauthButtonText;
return json;
}
/// Returns a new [ServerConfigDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ServerConfigDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return ServerConfigDto(
loginPageMessage: mapValueOfType<String>(json, r'loginPageMessage')!,
mapTileUrl: mapValueOfType<String>(json, r'mapTileUrl')!,
oauthButtonText: mapValueOfType<String>(json, r'oauthButtonText')!,
);
}
return null;
}
static List<ServerConfigDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ServerConfigDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ServerConfigDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ServerConfigDto> mapFromJson(dynamic json) {
final map = <String, ServerConfigDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ServerConfigDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ServerConfigDto-objects as value to a dart map
static Map<String, List<ServerConfigDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ServerConfigDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ServerConfigDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'loginPageMessage',
'mapTileUrl',
'oauthButtonText',
};
}

View File

@@ -16,6 +16,7 @@ class ServerFeaturesDto {
required this.clipEncode,
required this.configFile,
required this.facialRecognition,
required this.map,
required this.oauth,
required this.oauthAutoLaunch,
required this.passwordLogin,
@@ -30,6 +31,8 @@ class ServerFeaturesDto {
bool facialRecognition;
bool map;
bool oauth;
bool oauthAutoLaunch;
@@ -47,6 +50,7 @@ class ServerFeaturesDto {
other.clipEncode == clipEncode &&
other.configFile == configFile &&
other.facialRecognition == facialRecognition &&
other.map == map &&
other.oauth == oauth &&
other.oauthAutoLaunch == oauthAutoLaunch &&
other.passwordLogin == passwordLogin &&
@@ -60,6 +64,7 @@ class ServerFeaturesDto {
(clipEncode.hashCode) +
(configFile.hashCode) +
(facialRecognition.hashCode) +
(map.hashCode) +
(oauth.hashCode) +
(oauthAutoLaunch.hashCode) +
(passwordLogin.hashCode) +
@@ -68,13 +73,14 @@ class ServerFeaturesDto {
(tagImage.hashCode);
@override
String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
String toString() => 'ServerFeaturesDto[clipEncode=$clipEncode, configFile=$configFile, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, search=$search, sidecar=$sidecar, tagImage=$tagImage]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'clipEncode'] = this.clipEncode;
json[r'configFile'] = this.configFile;
json[r'facialRecognition'] = this.facialRecognition;
json[r'map'] = this.map;
json[r'oauth'] = this.oauth;
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
json[r'passwordLogin'] = this.passwordLogin;
@@ -95,6 +101,7 @@ class ServerFeaturesDto {
clipEncode: mapValueOfType<bool>(json, r'clipEncode')!,
configFile: mapValueOfType<bool>(json, r'configFile')!,
facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
map: mapValueOfType<bool>(json, r'map')!,
oauth: mapValueOfType<bool>(json, r'oauth')!,
oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
passwordLogin: mapValueOfType<bool>(json, r'passwordLogin')!,
@@ -151,6 +158,7 @@ class ServerFeaturesDto {
'clipEncode',
'configFile',
'facialRecognition',
'map',
'oauth',
'oauthAutoLaunch',
'passwordLogin',

View File

@@ -16,6 +16,7 @@ class SystemConfigDto {
required this.ffmpeg,
required this.job,
required this.machineLearning,
required this.map,
required this.oauth,
required this.passwordLogin,
required this.storageTemplate,
@@ -28,6 +29,8 @@ class SystemConfigDto {
SystemConfigMachineLearningDto machineLearning;
SystemConfigMapDto map;
SystemConfigOAuthDto oauth;
SystemConfigPasswordLoginDto passwordLogin;
@@ -41,6 +44,7 @@ class SystemConfigDto {
other.ffmpeg == ffmpeg &&
other.job == job &&
other.machineLearning == machineLearning &&
other.map == map &&
other.oauth == oauth &&
other.passwordLogin == passwordLogin &&
other.storageTemplate == storageTemplate &&
@@ -52,19 +56,21 @@ class SystemConfigDto {
(ffmpeg.hashCode) +
(job.hashCode) +
(machineLearning.hashCode) +
(map.hashCode) +
(oauth.hashCode) +
(passwordLogin.hashCode) +
(storageTemplate.hashCode) +
(thumbnail.hashCode);
@override
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'ffmpeg'] = this.ffmpeg;
json[r'job'] = this.job;
json[r'machineLearning'] = this.machineLearning;
json[r'map'] = this.map;
json[r'oauth'] = this.oauth;
json[r'passwordLogin'] = this.passwordLogin;
json[r'storageTemplate'] = this.storageTemplate;
@@ -83,6 +89,7 @@ class SystemConfigDto {
ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
job: SystemConfigJobDto.fromJson(json[r'job'])!,
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
map: SystemConfigMapDto.fromJson(json[r'map'])!,
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
@@ -137,6 +144,7 @@ class SystemConfigDto {
'ffmpeg',
'job',
'machineLearning',
'map',
'oauth',
'passwordLogin',
'storageTemplate',

View File

@@ -0,0 +1,106 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SystemConfigMapDto {
/// Returns a new [SystemConfigMapDto] instance.
SystemConfigMapDto({
required this.enabled,
required this.tileUrl,
});
bool enabled;
String tileUrl;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigMapDto &&
other.enabled == enabled &&
other.tileUrl == tileUrl;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(tileUrl.hashCode);
@override
String toString() => 'SystemConfigMapDto[enabled=$enabled, tileUrl=$tileUrl]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'tileUrl'] = this.tileUrl;
return json;
}
/// Returns a new [SystemConfigMapDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigMapDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return SystemConfigMapDto(
enabled: mapValueOfType<bool>(json, r'enabled')!,
tileUrl: mapValueOfType<String>(json, r'tileUrl')!,
);
}
return null;
}
static List<SystemConfigMapDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigMapDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigMapDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigMapDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigMapDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigMapDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigMapDto-objects as value to a dart map
static Map<String, List<SystemConfigMapDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigMapDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SystemConfigMapDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'tileUrl',
};
}

View File

@@ -0,0 +1,37 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for ServerConfigDto
void main() {
// final instance = ServerConfigDto();
group('test ServerConfigDto', () {
// String loginPageMessage
test('to test the property `loginPageMessage`', () async {
// TODO
});
// String mapTileUrl
test('to test the property `mapTileUrl`', () async {
// TODO
});
// String oauthButtonText
test('to test the property `oauthButtonText`', () async {
// TODO
});
});
}

View File

@@ -31,6 +31,11 @@ void main() {
// TODO
});
// bool map
test('to test the property `map`', () async {
// TODO
});
// bool oauth
test('to test the property `oauth`', () async {
// TODO

View File

@@ -17,6 +17,11 @@ void main() {
// final instance = ServerInfoApi();
group('tests for ServerInfoApi', () {
//Future<ServerConfigDto> getServerConfig() async
test('test getServerConfig', () async {
// TODO
});
//Future<ServerFeaturesDto> getServerFeatures() async
test('test getServerFeatures', () async {
// TODO

View File

@@ -31,6 +31,11 @@ void main() {
// TODO
});
// SystemConfigMapDto map
test('to test the property `map`', () async {
// TODO
});
// SystemConfigOAuthDto oauth
test('to test the property `oauth`', () async {
// TODO

View File

@@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for SystemConfigMapDto
void main() {
// final instance = SystemConfigMapDto();
group('test SystemConfigMapDto', () {
// bool enabled
test('to test the property `enabled`', () async {
// TODO
});
// String tileUrl
test('to test the property `tileUrl`', () async {
// TODO
});
});
}

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.77.0+100
version: 1.78.0+101
isar_version: &isar_version 3.1.0+1
environment:

View File

@@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
@@ -17,7 +18,6 @@ void main() {
required String checksum,
String? localId,
String? remoteId,
int deviceId = 1,
int ownerId = 590700560494856554, // hash of "1"
}) {
final DateTime date = DateTime(2000);
@@ -46,6 +46,7 @@ void main() {
UserSchema,
StoreValueSchema,
LoggerMessageSchema,
ETagSchema,
],
maxSizeMiB: 256,
directory: ".",
@@ -73,8 +74,8 @@ void main() {
await Store.put(StoreKey.currentUser, owner);
});
final List<Asset> initialAssets = [
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
makeAsset(checksum: "a", remoteId: "0-1"),
makeAsset(checksum: "b", remoteId: "2-1"),
makeAsset(checksum: "c", localId: "1", remoteId: "1-1"),
makeAsset(checksum: "d", localId: "2"),
makeAsset(checksum: "e", localId: "3"),
@@ -88,12 +89,13 @@ void main() {
test('test inserting existing assets', () async {
SyncService s = SyncService(db, hs);
final List<Asset> remoteAssets = [
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
makeAsset(checksum: "a", remoteId: "0-1"),
makeAsset(checksum: "b", remoteId: "2-1"),
makeAsset(checksum: "c", remoteId: "1-1"),
];
expect(db.assets.countSync(), 5);
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
final bool c1 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
expect(c1, false);
expect(db.assets.countSync(), 5);
});
@@ -101,15 +103,16 @@ void main() {
test('test inserting new assets', () async {
SyncService s = SyncService(db, hs);
final List<Asset> remoteAssets = [
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
makeAsset(checksum: "b", remoteId: "2-1", deviceId: 2),
makeAsset(checksum: "a", remoteId: "0-1"),
makeAsset(checksum: "b", remoteId: "2-1"),
makeAsset(checksum: "c", remoteId: "1-1"),
makeAsset(checksum: "d", remoteId: "1-2"),
makeAsset(checksum: "f", remoteId: "1-4"),
makeAsset(checksum: "g", remoteId: "3-1", deviceId: 3),
makeAsset(checksum: "g", remoteId: "3-1"),
];
expect(db.assets.countSync(), 5);
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
final bool c1 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
expect(c1, true);
expect(db.assets.countSync(), 7);
});
@@ -117,31 +120,56 @@ void main() {
test('test syncing duplicate assets', () async {
SyncService s = SyncService(db, hs);
final List<Asset> remoteAssets = [
makeAsset(checksum: "a", remoteId: "0-1", deviceId: 0),
makeAsset(checksum: "a", remoteId: "0-1"),
makeAsset(checksum: "b", remoteId: "1-1"),
makeAsset(checksum: "c", remoteId: "2-1", deviceId: 2),
makeAsset(checksum: "h", remoteId: "2-1b", deviceId: 2),
makeAsset(checksum: "i", remoteId: "2-1c", deviceId: 2),
makeAsset(checksum: "j", remoteId: "2-1d", deviceId: 2),
makeAsset(checksum: "c", remoteId: "2-1"),
makeAsset(checksum: "h", remoteId: "2-1b"),
makeAsset(checksum: "i", remoteId: "2-1c"),
makeAsset(checksum: "j", remoteId: "2-1d"),
];
expect(db.assets.countSync(), 5);
final bool c1 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
final bool c1 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
expect(c1, true);
expect(db.assets.countSync(), 8);
final bool c2 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
final bool c2 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
expect(c2, false);
expect(db.assets.countSync(), 8);
remoteAssets.removeAt(4);
final bool c3 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
final bool c3 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
expect(c3, true);
expect(db.assets.countSync(), 7);
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e", deviceId: 2));
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2", deviceId: 2));
final bool c4 = await s.syncRemoteAssetsToDb(owner, () => remoteAssets);
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
final bool c4 =
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
expect(c4, true);
expect(db.assets.countSync(), 9);
});
test('test efficient sync', () async {
SyncService s = SyncService(db, hs);
final List<Asset> toUpsert = [
makeAsset(checksum: "a", remoteId: "0-1"), // changed
makeAsset(checksum: "f", remoteId: "0-2"), // new
makeAsset(checksum: "g", remoteId: "0-3"), // new
];
toUpsert[0].isFavorite = true;
final List<String> toDelete = ["2-1", "1-1"];
final bool c = await s.syncRemoteAssetsToDb(
owner,
(user, since) async => (toUpsert, toDelete),
(user) => throw Exception(),
);
expect(c, true);
expect(db.assets.countSync(), 6);
});
});
}
Future<(List<Asset>?, List<String>?)> _failDiff(User user, DateTime time) =>
Future.value((null, null));
class MockHashService extends Mock implements HashService {}

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