Compare commits

...

53 Commits

Author SHA1 Message Date
Alex
9996a1ec3e fix: prevent database deadlock (eps 2) 2025-09-08 21:10:51 -05:00
Sergey Katsubo
23fb2e0fae chore(server): fix logger context in StorageCore (#21534)
* Fix logging context in StorageCore

* Revert "Fix logging context in StorageCore"

This reverts commit 0aa540afd1.

* Fix logging context in StorageCore - one line change
2025-09-08 17:28:40 -04:00
Alex
59accbf32a fix: prevent isolate deadlock (#21692) 2025-09-08 19:18:13 +00:00
shenlong
059a0e8aa8 feat: sync AuthUserV1 (#21565)
* feat: sync AuthUserV1

* migration

* chore: fix analyze

* fix user updatedAt check

* fix: auth user sync query

* generate sql

* bump schema version and update migration

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-09-08 14:00:10 -05:00
bo0tzz
6a55c36762 fix: print errors in weblate merge job (#21683) 2025-09-08 17:37:10 +02:00
bo0tzz
c0bff4b493 fix: pass repo to gh cli in weblate checks job (#21681) 2025-09-08 17:32:44 +02:00
shenlong
fd4c2acde8 feat: handle SyncResetV1 (#20732)
* feat: handle SyncResetV1

* auto retry on reset and handle SyncCompleteV1

* fix tests

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-08 09:48:26 -05:00
bo0tzz
5acf909235 fix: gh cli needs explicit GH_TOKEN env var in gha (#21680) 2025-09-08 14:48:08 +00:00
bo0tzz
fb1458c720 fix: pre-job branch scope on weblate checks job (#21679) 2025-09-08 16:30:28 +02:00
Peter Buga
255dabc239 fix(server): valid backups with DB_URL env variable config (#21669) 2025-09-08 14:29:34 +00:00
Mert
27751f8fd4 fix(server): remove pcm from default accepted codecs (#21655)
remove pcm from default
2025-09-08 09:26:28 -05:00
bo0tzz
72ffa37dd9 feat: workflow for automated translations merge (#21639)
* feat: workflow for automated translations merge

* feat: dismiss review on merge failure

* chore: parameterize weblate URL

* fix: remove unnecessary CHANGES_REQUESTED review flow

* feat: leave weblate locked on failures

* chore: remove unnecessary merge timeout comment

The review dismissal already communicates this

* chore: remove todo

* feat: save api call

* fix: quotes
2025-09-08 09:25:31 -05:00
Jason Rasmussen
5a7042364b feat: add partner create endpoint (#21625) 2025-09-05 17:59:11 -04:00
github-actions
db0ea0f3a8 chore: version v1.141.1 2025-09-05 19:44:39 +00:00
Alex
88c0243a20 chore: robust isolation tasks coordination (#21605)
* chore: robust isolation tasks coordination

* give more time for database transaction to clean up

* chore: clean up logs

* chore: clean up logs

* fix: logs
2025-09-05 19:39:38 +00:00
Alex
3a29522df6 fix: memory not getting refreshed on new day (#21623) 2025-09-05 19:32:41 +00:00
bo0tzz
50eae23f3a fix: use specific uv version in ML dockerfile (#21611) 2025-09-05 20:15:29 +02:00
Weblate (bot)
95419750bb chore(web): update translations (#21464)
Co-authored-by: AbuKareem Tuffaha <abukareem.tuffaha@gmail.com>
Co-authored-by: Dawider10 <dawider110@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Gustavo de León <alfonso.gus.deleon@gmail.com>
Co-authored-by: Hyouhyan <hyouhyan@hyouhyan.com>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: Isfan Nur Fauzi <isfannurfauzi@gmail.com>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: JPar99 <github.wad969@passmail.com>
Co-authored-by: Joseph <josephlegrand33+hosted.weblate.org@gmail.com>
Co-authored-by: José Rodrigues <j.rodrigues.pcmedic@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Leigh van der merwe <palitu822@gmail.com>
Co-authored-by: Lenny Angst <lenny@familie-angst.ch>
Co-authored-by: Leon Fertig <leon.fertig2004@gmail.com>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Mikko Asikainen <mikko@asikainen.com>
Co-authored-by: Murad Bashirov <carlsonmu@gmail.com>
Co-authored-by: Mārtiņš Bruņenieks <martinsb@gmail.com>
Co-authored-by: Phantom0174 <darrenhsiou@gmail.com>
Co-authored-by: PontusÖsterlindh <pontus@osterlindh.com>
Co-authored-by: Sergey Katsubo <skatsubo@gmail.com>
Co-authored-by: Sneha George <snehavg94@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Tage Lauritsen <tage@tunenet.dk>
Co-authored-by: Tomi Pöyskö <tomi.poysko@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vegard Fladby <vegard@fladby.org>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: anton garcias <isaga.percompartir@gmail.com>
Co-authored-by: czlevi7 <czlevi7@gmail.com>
Co-authored-by: gablilli <gabriele.lilli0511@gmail.com>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
2025-09-05 19:53:36 +02:00
renovate[bot]
5fb858a865 chore(deps): update node.js to v22.19.0 (#21509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 08:43:58 -04:00
Jason Rasmussen
18084a49ec chore: mise tasks (#21597) 2025-09-04 18:10:36 -04:00
github-actions
f107cb044a chore: version v1.141.0 2025-09-04 19:42:02 +00:00
renovate[bot]
f4e7ea47a6 chore(deps): update dependency @types/node to ^22.18.0 (#21506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 21:26:06 +02:00
Riccardo Ruspoli
8747fc4935 fix(server): consider asset creation date when EXIF is missing (#21586)
* fix(server): fallback to asset.fileCreatedAt when EXIF is missing

* merge main

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-04 19:18:12 +00:00
Alex
287fa79d75 fix: remove unnecessary call to create remote album (#21599) 2025-09-04 14:11:51 -05:00
Alex
bcfb5bee1f feat: album info sync (#21103)
* wip

* album creation

* fix: album api repository no invalidating after logging out

* add linkedRemoteAlbumId column and migration

* link/unlink remote album

* logic to find and add new assets to album

* pr feedback

* add toggle option to backup option page

* refactor: provider > service

* rename

* Handle page pop manually

* UI feedback for user creation and sync linked album

* uncomment migration

* remove unused method
2025-09-04 18:44:10 +00:00
Yaros
538263dc38 fix(mobile): location button map beta timeline (#21590)
fix(mobile): location button map
2025-09-04 13:26:16 -05:00
Maksim
51aec1e93d fix(mobile): Correction of image creation date by using mtime instead… (#21508)
* fix(mobile): Correction of image creation date by using mtime instead of ctime.

* use the timestamps from the asset for uploads

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-04 18:26:02 +00:00
Mert
53825cc3d6 chore(deps): bump sharp to 0.34.3 (#21596)
* bump sharp to 0.34.3

* set unlimited
2025-09-04 13:23:13 -05:00
Jason Rasmussen
6e7c2817a3 fix: asset upload metadata validation (#21594) 2025-09-04 13:22:01 -05:00
bo0tzz
7bd79b551c feat: use mise for core dev tools (#21566)
* feat: use mise for core tools

* feat: mise handle dart

* feat: install dcm through mise

* fix: enable experimental in mise config

* feat: use mise.lock

* chore: always pin mise use

---------

Co-authored-by: bwees <brandonwees@gmail.com>
2025-09-04 12:58:42 -04:00
shenlong
5fe954b3c9 fix: use lock to synchronise foreground and background backup (#21522)
* fix: use lock to synchronise foreground and background backup

# Conflicts:
#	mobile/lib/domain/services/background_worker.service.dart
#	mobile/lib/platform/background_worker_api.g.dart
#	mobile/pigeon/background_worker_api.dart

* add timeout to the splash-screen acquire lock

* fix: null check on created date

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-04 11:44:33 -05:00
Jason Rasmussen
7f81a5bd6f fix: sidecar check job (#21312) 2025-09-04 16:23:58 +00:00
Arthur Normand
37a79292c0 feat: view similar photos (#21108)
* Enable filteing by example

* Drop `@GenerateSql` for `getEmbedding`?

* Improve error message

* PR Feedback

* Sort en.json

* Add SQL

* Fix lint

* Drop test that is no longer valid

* Fix i18n file sorting

* Fix TS error

* Add a `requireAccess` before pulling the embedding

* Fix decorators

* Run `make open-api`

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-09-04 09:22:09 -05:00
Brandon Wees
bf6211776f fix: retain filter and sort options when pulling to refresh (#21452)
* fix: retain filter and sort options when pulling to refresh

* chore: use classes to manage state

* chore: format

* chore: refactor to keep local state of filter/sorted albums instead of a global filteredAlbums

* fix: keep sort when page is navigated away and returned

* chore: lint

* chore: format

why is autoformat not working

* fix: default sort direction state

* fix: search clears sorting

we have to cache our sorted albums since sorting is very computationally expensive and cannot be run on every keystroke. For searches, instead of pulling from the list of albums, we now pull from the cached sorted list and then filter which is then shown to the user
2025-09-04 09:08:17 -05:00
waclaw66
6c178a04dc fix(mobile): pinch + move scale (#21332)
* fix: pinch + move scale

* added lost changes from #18744
2025-09-04 09:01:39 -05:00
Snowknight26
036d314cb6 fix(web): Make Manage location utility header responsive (#21480)
* fix(web): Make Manage location utility header responsive

* Consolidate <p> into <Text>
2025-09-04 08:59:26 -05:00
Noel S
1fc5da398a fix(mobile): Hide system UI when entering immersive mode in asset viewer (#21539)
Implement hiding system ui in asset viewer
2025-09-04 08:57:34 -05:00
Sudheer Reddy Puthana
4d84338086 fix(mobile): readonly mode fixes (#21545)
* fix: Enables videotimeline in readonly mode

- Enables only the video controls in the bottom bar when readonlyMode is enabled.
- Fixes the message on the app profile bar when readOnlyMode is enabled **but** betaTimeline is not enabled.

Fixes https://github.com/immich-app/immich/issues/21441

Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com>

* cleanup bottom bar handling

---------

Signed-off-by: Sudheer Puthana <Sud-Puth@users.noreply.github.com>
Co-authored-by: bwees <brandonwees@gmail.com>
2025-09-04 08:50:38 -05:00
Yaros
0ac49b00ee feat(mobile): scrubber haptics (beta timeline) (#21351)
* feat(mobile): scrubber haptics beta timeline

* changed haptic to selectionClick
2025-09-04 08:47:16 -05:00
Mert
e427778a96 fix(mobile): pause image loading on inactive state (#21543)
* pause image loading

* make thumbhashes wait too
2025-09-04 08:40:38 -05:00
Pedro Simão
b82e29fbb4 feat(mobile): add to albums from existing albums (#21554)
* feat(mobile): add to albums from existing albums

* formatted files

* used the new t() method for translation

* removed unused import
2025-09-04 08:39:10 -05:00
shenlong
ff19aea4ac fix: keyboard not dismissed in places page (#21583)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-04 08:38:44 -05:00
Jason Rasmussen
28179a3a1d feat: audit cleanup (#21567) 2025-09-03 22:50:27 +00:00
Min Idzelis
af1e18d07e fix: docker upload_location perm fix for dev (#21501) 2025-09-03 18:27:30 +01:00
shenlong
270a0ff986 chore: log name and createdAt of asset on hash failures (#21546)
* chore: log name and createdAt of asset on hash failures

* add album name to hash failure logs

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-03 09:58:03 -05:00
shenlong
9d3f10372d refactor: simplify background worker (#21558)
* chore: log hash starting

* chore: android - bump the min worker delay

* remove local sync only task and always enqueue background workers

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-03 09:57:30 -05:00
bo0tzz
2f1385a236 chore: request LLM disclosure in PR template (#21553)
Suggestions for different wording/placeholder are welcome
2025-09-03 09:11:24 -05:00
renovate[bot]
183a285584 chore(deps): update base-image to v202509021104 (major) (#21513)
chore(deps): update base-image to v202509021104

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 21:20:21 -05:00
Alex
5ce946bb5b fix: null check (#21536) 2025-09-02 19:21:41 -05:00
shenlong
674faf2e57 fix: local sync task never runs on iOS (#21491)
* fix: local sync task never runs on iOS

* chore: rename ios register method

* refactor from using dart callback to dart entrypoint + more logs

* check if file exists before hashing

* reschedule local sync task

* chore: rename background worker logger

* refactor: move file exists check inside repo

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-03 02:05:58 +05:30
Alex
4f7702c6bf fix: iOS portrait photo saved as jpg extension (#21388)
remove bad merged settings

remove console log
2025-09-02 14:26:12 -05:00
bo0tzz
28edf5664d fix: set specific AssetUpload permission on checkBulkUpload endpoint (#21470)
* fix: set specific AssetUpload permission on checkBulkUpload endpoint

Fixes #21456

* fix: make open-api
2025-09-02 14:21:14 -05:00
shenlong
ec2f94cae8 fix: handle datetime outside the valid range supported by dart (#21526)
* fix: handle datetime outside the valid range supported by dart

* add tests for tryFromSecondsSinceEpoch

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-02 14:20:49 -05:00
244 changed files with 21701 additions and 3336 deletions

2
.github/.nvmrc vendored
View File

@@ -1 +1 @@
22.18.0
22.19.0

View File

@@ -34,3 +34,7 @@ The `/api/something` endpoint is now `/api/something-else`
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
## Please describe to which degree, if any, an LLM was used in creating this pull request.
...

View File

@@ -0,0 +1,96 @@
name: Merge translations
on:
workflow_call:
workflow_dispatch:
permissions: {}
env:
WEBLATE_HOST: 'https://hosted.weblate.org'
WEBLATE_COMPONENT: 'immich/immich'
jobs:
merge:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Find translation PR
id: find_pr
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable | read PR
echo "$PR"
echo "$PR" | jq '
if length == 1 then
.[0].number
else
error("Expected exactly 1 entry, got \(length)")
end
' 2>&1 | read PR_NUMBER || exit 1
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "Selected PR $PR_NUMBER"
echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"' || { echo "PR is not mergeable" ; exit 1 }
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Lock weblate
env:
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
run: |
curl -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=true
- name: Commit translations
env:
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
run: |
curl -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=commit
curl -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/repository/" -d operation=push
- name: Merge PR
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
run: |
gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \
| jq '.id' | read REVIEW_ID
echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash
- name: Wait for PR to merge
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
REVIEW_ID: ${{ steps.merge_pr.outputs.REVIEW_ID }}
run: |
for i in {1..10}; do
if gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json merged | jq -e '.merged == true'; then
echo "PR merged"
exit 0
else
echo "PR not merged yet, waiting..."
sleep 6
fi
done
echo "PR did not merge in time"
gh api -X PUT "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews/$REVIEW_ID/dismissals" --field message='Merge attempt timed out' --field event='DISMISS'
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --disable-auto
exit 1
- name: Unlock weblate
env:
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
run: |
curl -X POST -H "Authorization: Token $WEBLATE_TOKEN" "$WEBLATE_HOST/api/components/$WEBLATE_COMPONENT/lock/" -d lock=false

View File

@@ -24,6 +24,15 @@ concurrency:
permissions: {}
jobs:
merge_translations:
uses: ./.github/workflows/merge-translations.yml
permissions:
pull-requests: write
secrets:
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
bump_version:
runs-on: ubuntu-latest
outputs:

View File

@@ -1,6 +1,7 @@
name: Weblate checks
on:
pull_request_review:
pull_request:
branches: [main]
@@ -12,7 +13,7 @@ jobs:
permissions:
contents: read
outputs:
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
should_run: ${{ steps.found_paths.outputs.i18n == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
@@ -32,19 +33,15 @@ jobs:
permissions: {}
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
steps:
- name: Check weblate lock
- name: Bot review status
env:
PR_NUMBER: ${{ github.event.pull_request.number || github.event.pull_request_review.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
if [[ "false" = $(curl https://hosted.weblate.org/api/components/immich/immich/lock/ | jq .locked) ]]; then
exit 1
fi
- name: Find Pull Request
uses: juliangruber/find-pull-request-action@952b3bb1ddb2dcc0aa3479e98bb1c2d1a922f096 # v1.10.0
id: find-pr
with:
branch: chore/translations
- name: Fail if existing weblate PR
if: ${{ steps.find-pr.outputs.number }}
run: exit 1
# Then check for APPROVED by the bot, if absent fail
gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json reviews | jq -e '.reviews | map(select(.author.login == "github-actions[bot]" and .state == "APPROVED")) | length > 0' \
|| (echo "The push-o-matic bot has not approved this PR yet" && exit 1)
success-check-lock:
name: Weblate Lock Check Success
needs: [enforce-lock]

View File

@@ -10,14 +10,14 @@ dev-update: prepare-volumes
dev-scale: prepare-volumes
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
dev-docs: prepare-volumes
dev-docs:
npm --prefix docs run start
.PHONY: e2e
e2e: prepare-volumes
e2e:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
e2e-update: prepare-volumes
e2e-update:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
e2e-down:
@@ -73,6 +73,8 @@ define safe_chown
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
true; \
else \
STATUS=$$?; echo "Exit code: $$STATUS $(1)"; \
echo "$$STATUS $(1)"; \
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
exit 1; \
fi;
@@ -83,11 +85,13 @@ prepare-volumes:
@$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
ifneq ($(UPLOAD_LOCATION),)
ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
@mkdir -p "docker/$(UPLOAD_LOCATION)"
@mkdir -p "docker/$(UPLOAD_LOCATION)/photos/upload"
@$(call safe_chown,docker/$(UPLOAD_LOCATION),)
@$(call safe_chown,docker/$(UPLOAD_LOCATION)/photos,-R)
else
@mkdir -p "$(UPLOAD_LOCATION)"
@mkdir -p "$(UPLOAD_LOCATION)/photos/upload"
@$(call safe_chown,$(UPLOAD_LOCATION),)
@$(call safe_chown,$(UPLOAD_LOCATION)/photos,-R)
endif
endif

View File

@@ -1 +1 @@
22.18.0
22.19.0

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.86",
"version": "2.2.88",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.17.1",
"@types/node": "^22.18.0",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8"
},
"volta": {
"node": "22.18.0"
"node": "22.19.0"
}
}

View File

@@ -1 +1 @@
22.18.0
22.19.0

View File

@@ -60,6 +60,6 @@
"node": ">=20"
},
"volta": {
"node": "22.18.0"
"node": "22.19.0"
}
}

View File

@@ -1,4 +1,12 @@
[
{
"label": "v1.141.1",
"url": "https://v1.141.1.archive.immich.app"
},
{
"label": "v1.141.0",
"url": "https://v1.141.0.archive.immich.app"
},
{
"label": "v1.140.1",
"url": "https://v1.140.1.archive.immich.app"

View File

@@ -1 +1 @@
22.18.0
22.19.0

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.140.1",
"version": "1.141.1",
"description": "",
"main": "index.js",
"type": "module",
@@ -26,7 +26,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^22.17.1",
"@types/node": "^22.18.0",
"@types/oidc-provider": "^9.0.0",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
@@ -45,7 +45,7 @@
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.0",
"sharp": "^0.34.3",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
@@ -54,6 +54,6 @@
"vitest": "^3.0.0"
},
"volta": {
"node": "22.18.0"
"node": "22.19.0"
}
}

View File

@@ -23,8 +23,8 @@ describe('/partners', () => {
]);
await Promise.all([
createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
createPartner({ partnerCreateDto: { sharedWithId: user2.userId } }, { headers: asBearerAuth(user1.accessToken) }),
createPartner({ partnerCreateDto: { sharedWithId: user1.userId } }, { headers: asBearerAuth(user2.accessToken) }),
]);
});

View File

@@ -462,7 +462,8 @@ export const utils = {
updateLibrary: (accessToken: string, id: string, dto: UpdateLibraryDto) =>
updateLibrary({ id, updateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) =>
createPartner({ partnerCreateDto: { sharedWithId: id } }, { headers: asBearerAuth(accessToken) }),
updateMyPreferences: (accessToken: string, userPreferencesUpdateDto: UserPreferencesUpdateDto) =>
updateMyPreferences({ userPreferencesUpdateDto }, { headers: asBearerAuth(accessToken) }),

View File

@@ -396,6 +396,7 @@
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
"advanced_settings_proxy_headers_subtitle": "عرف عناوين الوكيل التي يستخدمها Immich لارسال كل طلب شبكي",
"advanced_settings_proxy_headers_title": "عناوين الوكيل",
"advanced_settings_readonly_mode_title": "وضع القراءة فقط",
"advanced_settings_self_signed_ssl_subtitle": "تخطي التحقق من شهادة SSL لخادم النقطة النهائي. مكلوب للشهادات الموقعة ذاتيا.",
"advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا",
"advanced_settings_sync_remote_deletions_subtitle": "حذف او استعادة تلقائي للاصول على هذا الجهاز عند تنفيذ العملية على الويب",

View File

@@ -1,37 +1,53 @@
{
"about": "Haqqinda",
"about": "Haqqında",
"account": "Hesab",
"account_settings": "Hesab parametrləri",
"acknowledge": "Təsdiq et",
"action": "Əməliyyat",
"action_common_update": "Yenilə",
"actions": "Əməliyyatlar",
"active": "Aktiv",
"activity": "Fəaliyyət",
"activity_changed": "Fəaliyyət {enabled, select, true {aktivdir} other {aktiv deyil}}",
"add": "Əlavə et",
"add_a_description": "Təsviri əlavə et",
"add_a_location": "Məkan əlavə et",
"add_a_name": "Ad əlavə et",
"add_a_title": "Başlıq əlavə et",
"add_birthday": "Doğum günü əlavə et",
"add_endpoint": "Son nöqtə əlavə et",
"add_exclusion_pattern": "İstisna nümunəsi əlavə et",
"add_import_path": "Import yolunu əlavə et",
"add_location": "Məkanı əlavə et",
"add_location": "Məkan əlavə et",
"add_more_users": "Daha çox istifadəçi əlavə et",
"add_partner": "Partnyor əlavə et",
"add_path": "Yol əlavə et",
"add_photos": "Şəkilləri əlavə et",
"add_to": "... əlavə et",
"add_photos": "Şəkillər əlavə et",
"add_tag": "Etiket əlavə et",
"add_to": "Bura əlavə et…",
"add_to_album": "Albom əlavə et",
"add_to_album_bottom_sheet_added": "{album} albomuna əlavə edildi",
"add_to_album_bottom_sheet_already_exists": "Artıq {album} albomunda var",
"add_to_album_toggle": "{album} üçün seçimi dəyişin",
"add_to_albums": "Albomlara əlavə et",
"add_to_albums_count": "Albomlara əlavə et ({count})",
"add_to_shared_album": "Paylaşılan alboma əlavə et",
"add_url": "URL əlavə et",
"added_to_archive": "Arxivə əlavə edildi",
"added_to_favorites": "Sevimlilələrə əlavə edildi",
"added_to_favorites_count": "{count, number} şəkil sevimlilələrə əlavə edildi",
"admin": {
"add_exclusion_pattern_description": "İstisna şablonlarını əlavə edin. *, ** və ? ilə Globbing dəstəklənir. Məs.: \"Raw\" adlanan hər hansısa bir qovluqda bütün faylları saymamaq üçün \"**/Raw/**\"-dan istifadə edin. \".tif\" ilə bitən bütün faylları saymamaq üçün \"**/*.tif\"-dən istifadə edin. Faylı mütləq yoldan istifadə etməklə saymamaq istəyirsinizsə \"/path/to/ignore/**\"-dan istifadə edin.",
"admin_user": "Admin İstifadəçi",
"asset_offline_description": "Bu xarici kitabxana varlığı diskdə artıq tapılmadı və zibil qutusuna köçürüldü. Əgər fayl kitabxana içərisində köçürülübsə, zaman şkalanızı yeni uyğun gələn varlıq üçün yoxlayın. Varlığı yenidən qaytarmaq üçün aşağıda verilmiş fayl yolunun Immich tərəfindən əlçatan olduğundan əmin olduqdan sonra kitabxananı skan edin.",
"authentication_settings": "Səlahiyyətləndirmə parametrləri",
"authentication_settings_description": "Şifrə, OAuth və digər səlahiyyətləndirmə parametrləri",
"authentication_settings_disable_all": "Bütün giriş etmə metodlarını söndürmək istədiyinizdən əminsinizmi? Giriş etmə funksiyası tamamilə söndürüləcəkdir.",
"authentication_settings_reenable": "Yenidən aktiv etmək üçün <link> Server Əmri</link> -ni istifadə edin.",
"background_task_job": "Arxa plan tapşırıqları",
"backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
"backup_database": "Verilənlər bazasının dump-ını yaradın",
"backup_database_enable_description": "Verilənlər bazasının artıq nüsxələrini aktiv et",
"backup_keep_last_amount": "Tutulması gərəkən nüsxələrin sayı",
"backup_settings": "Ehtiyat Nüsxə Parametrləri",
"backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
"config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
@@ -84,5 +100,6 @@
"machine_learning_facial_recognition": "Üz Tanıma",
"machine_learning_facial_recognition_description": "Şəkillərdəki üzləri aşkarla, tanı və qruplaşdır",
"machine_learning_facial_recognition_model": "Üz tanıma modeli"
}
},
"timeline": "Zaman şkalası"
}

View File

@@ -399,6 +399,8 @@
"purchase_button_buy": "Купіць",
"purchase_button_buy_immich": "Купіць Immich",
"purchase_button_select": "Выбраць",
"readonly_mode_enabled": "Уключаны рэжым толькі для чытання",
"reassign": "Перапрызначыць",
"remove": "Выдаліць",
"remove_from_album": "Выдаліць з альбома",
"remove_from_favorites": "Выдаліць з абраных",

View File

@@ -396,6 +396,8 @@
"advanced_settings_prefer_remote_title": "Предпочитай изображенията на сървъра",
"advanced_settings_proxy_headers_subtitle": "Дефиниране на прокси хедъри, които Immich трябва да изпраща с всяка мрежова заявка",
"advanced_settings_proxy_headers_title": "Прокси хедъри",
"advanced_settings_readonly_mode_subtitle": "Активира режима \"само за четене\", при който снимките могат да бъдат разглеждани, но неща като избор на няколко изображения, споделяне, изтриване са забранени. Активиране/деактивиране на режима само за четене става от картинката-аватар на потребителя от основния екран",
"advanced_settings_readonly_mode_title": "Режим само за четене",
"advanced_settings_self_signed_ssl_subtitle": "Пропуска проверката на SSL-сертификата на сървъра. Изисква се при самоподписани сертификати.",
"advanced_settings_self_signed_ssl_title": "Разреши самоподписани SSL сертификати",
"advanced_settings_sync_remote_deletions_subtitle": "Автоматично изтрии или възстанови обект на това устройство, когато действието е извършено през уеб-интерфейса",
@@ -461,6 +463,7 @@
"app_bar_signout_dialog_title": "Излез от профила",
"app_settings": "Настройки ма приложението",
"appears_in": "Излиза в",
"apply_count": "Приложи ({count, number})",
"archive": "Архив",
"archive_action_prompt": "{count} са добавени в Архива",
"archive_or_unarchive_photo": "Архивиране или деархивиране на снимка",
@@ -500,7 +503,7 @@
"assets": "Елементи",
"assets_added_count": "Добавено {count, plural, one {# asset} other {# assets}}",
"assets_added_to_album_count": "Добавен(и) са {count, plural, one {# актив} other {# актива}} в албума",
"assets_added_to_albums_count": "Добавени са {assetTotal} обекта в {albumTotal} албума",
"assets_added_to_albums_count": "{assetTotal, plural, one {# обект е добавен} other {# обекта са добавени}} в {albumTotal, plural, one {# албум} other {# албума}}",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Обекта не може да се добави} other {Обектите не може да се добавят}} в албума",
"assets_cannot_be_added_to_albums": "{count, plural, one {обект не може да бъде добавен} other {обекта не могат да бъдат добавени}} в никой от албумите",
"assets_count": "{count, plural, one {# актив} other {# актива}}",
@@ -912,7 +915,7 @@
"error_selecting_all_assets": "Грешка при избора на всички файлове",
"exclusion_pattern_already_exists": "Този модел за изключване вече съществува.",
"failed_to_create_album": "Неуспешно създаване на албум",
"failed_to_create_shared_link": "Неуспешно създаване на споделена връзка",
"failed_to_create_shared_link": "Неуспешно създаване на спoделена връзка",
"failed_to_edit_shared_link": "Неуспешно редактиране на споделена връзка",
"failed_to_get_people": "Неуспешно зареждане на хора",
"failed_to_keep_this_delete_others": "Неуспешно запазване на този обект и изтриване на останалите обекти",
@@ -1073,12 +1076,18 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "За да работи тази функция зарежда външни ресурси от Google.",
"general": "Общи",
"geolocation_instruction_all_have_location": "Всички обекти от тази дата вече имат данни за местоположение. Опитайте да включите показване на всички обекти или изберете друга дата",
"geolocation_instruction_location": "Изберете обект с GPS координати за да използвате тях или изберете място директно от картата",
"geolocation_instruction_no_date": "Изберете дата за да управлявате данните за локация на снимките и видеата от тази дата",
"geolocation_instruction_no_photos": "Не са намерени снимки или видеа от тази дата. Изберете друга дата",
"get_help": "Помощ",
"get_wifiname_error": "Неуспешно получаване името на Wi-Fi мрежата. Моля, убедете се, че са предоставени нужните разрешения на приложението и има връзка с Wi-Fi",
"getting_started": "Как да започнем",
"go_back": "Връщане назад",
"go_to_folder": "Отиди в папката",
"go_to_search": "Преминаване към търсене",
"gps": "GPS координати",
"gps_missing": "Няма GPS координати",
"grant_permission": "Дай разрешение",
"group_albums_by": "Групирай албум по...",
"group_country": "Групирай по държава",
@@ -1262,6 +1271,7 @@
"main_branch_warning": "Използвате версия за разработчици, силно препоръчваме да използвате официална версия!",
"main_menu": "Главно меню",
"make": "Марка",
"manage_geolocation": "Управление на местоположенията",
"manage_shared_links": "Управление на споделени връзки",
"manage_sharing_with_partners": "Управление на споделянето с партньори",
"manage_the_app_settings": "Управление на настройките на приложението",
@@ -1508,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "Мобилното приложение е остаряло. Моля, актуализирай до най-новата версия.",
"profile_drawer_client_server_up_to_date": "Клиента и сървъра са обновени",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Режима само за четене е активиран. С двоен клик върху картиката-аватар на потребителя ще деактивирате само за четене.",
"profile_drawer_server_out_of_date_major": "Версията на сървъра е остаряла. Моля, актуализирай поне до последната главна версия.",
"profile_drawer_server_out_of_date_minor": "Версията на сървъра е остаряла. Моля, актуализирай до последната версия.",
"profile_image_of_user": "Профилна снимка на {user}",
@@ -1553,6 +1564,8 @@
"rating_description": "Покажи EXIF оценката в панела с информация",
"reaction_options": "Избор на реакция",
"read_changelog": "Прочети промените",
"readonly_mode_disabled": "Режима само за четене е деактивиран",
"readonly_mode_enabled": "Режима само за четене е активиран",
"reassign": "Преназначаване",
"reassigned_assets_to_existing_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на {name, select, null {съществуващ човек} other {{name}}}",
"reassigned_assets_to_new_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на нов човек",
@@ -1722,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "Създаването на албум не бе успешно",
"selected": "Избрано",
"selected_count": "{count, plural, other {# избрани}}",
"selected_gps_coordinates": "избрани GPS координати",
"send_message": "Изпратете съобщение",
"send_welcome_email": "Изпратете имейл за добре дошли",
"server_endpoint": "Адрес на сървъра",
@@ -1832,8 +1846,10 @@
"shift_to_permanent_delete": "Натиснете ⇧, за да изтриете завинаги елемента",
"show_album_options": "Показване опции за албум",
"show_albums": "Покажи албуми",
"show_all_assets": "Покажи всичко",
"show_all_people": "Покажи всички хора",
"show_and_hide_people": "Показване и скриване на хора",
"show_assets_without_location": "Покажи обекти без координати",
"show_file_location": "Покажи местоположението на файла",
"show_gallery": "Покажи галерия",
"show_hidden_people": "Показване на скритите хора",
@@ -1941,7 +1957,9 @@
"to_change_password": "Промяна на паролата",
"to_favorite": "Любим",
"to_login": "Вписване",
"to_multi_select": "за избор на няколко",
"to_parent": "Отиди към родителския елемент",
"to_select": "за избор",
"to_trash": "Кошче",
"toggle_settings": "Превключване на настройките",
"total": "Общо",
@@ -1991,6 +2009,7 @@
"unstacked_assets_count": "Разкачени {count, plural, one {# елемент} other {# елементи}}",
"untagged": "Немаркирани",
"up_next": "Следващ",
"update_location_action_prompt": "Обнови координатите на {count} избрани обекта с:",
"updated_at": "Обновено",
"updated_password": "Паролата е актуализирана",
"upload": "Качване",
@@ -2015,6 +2034,7 @@
"use_biometric": "Използвай биометрия",
"use_current_connection": "използвай текущата връзка",
"use_custom_date_range": "Използвайте собствен диапазон от дати вместо това",
"use_this_location": "Избери това място",
"user": "Потребител",
"user_has_been_deleted": "Този потребител е премахнат.",
"user_id": "Потребител ИД",

View File

@@ -14,5 +14,10 @@
"add_exclusion_pattern": "Putem wan paten wae hemi karem aot",
"add_import_path": "Putem wan pat blo import",
"add_location": "Putem wan place blo hem",
"add_more_users": "Putem mor man"
"add_more_users": "Putem mor man",
"readonly_mode_enabled": "Mod blo yu no save janjem i on",
"reassigned_assets_to_new_person": "Janjem{count, plural, one {# asset} other {# assets}} blo nu man",
"reassing_hint": "janjem ol sumtin yu bin joos i go blo wan man",
"recent-albums": "album i no old tu mas",
"recent_searches": "lukabout wea i no old tu mas"
}

View File

@@ -396,6 +396,8 @@
"advanced_settings_prefer_remote_title": "Prefereix imatges remotes",
"advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa",
"advanced_settings_proxy_headers_title": "Capçaleres de proxy",
"advanced_settings_readonly_mode_subtitle": "Habilita el només de lectura mode on les fotos poden ser només vist, a coses els agrada seleccionant imatges múltiples, compartint, càsting, elimina és tot discapacitat. Habilita/Desactiva només de lectura via avatar d'usuari des de la pantalla major",
"advanced_settings_readonly_mode_title": "Mode de només lectura",
"advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.",
"advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats",
"advanced_settings_sync_remote_deletions_subtitle": "Suprimeix o restaura automàticament un actiu en aquest dispositiu quan es realitzi aquesta acció al web",
@@ -461,6 +463,7 @@
"app_bar_signout_dialog_title": "Tanca la sessió",
"app_settings": "Configuració de l'app",
"appears_in": "Apareix a",
"apply_count": "Aplicar ({count, number})",
"archive": "Arxiu",
"archive_action_prompt": "{count} afegit a Arxiu",
"archive_or_unarchive_photo": "Arxivar o desarxivar fotografia",
@@ -1073,12 +1076,18 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Aquesta funció carrega recursos externs de Google per funcionar.",
"general": "General",
"geolocation_instruction_all_have_location": "Tots els actius d'aquesta data ja tenen dades d'ubicació. Prova de mostrar tots els actius o selecciona una data diferent",
"geolocation_instruction_location": "Fes click en un element amb coordinades GPS per utilitzar la seva ubicació o selecciona una ubicació des del mapa",
"geolocation_instruction_no_date": "Seleccioneu una data per gestionar dades d'ubicació per a fotos i vídeos d'aquest dia",
"geolocation_instruction_no_photos": "Cap foto o vídeo trobats per a aquesta data. Selecciona una data diferent",
"get_help": "Aconseguir ajuda",
"get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi",
"getting_started": "Començant",
"go_back": "Torna",
"go_to_folder": "Anar al directori",
"go_to_search": "Vés a cercar",
"gps": "GPS",
"gps_missing": "Sense GPS",
"grant_permission": "Concedir permís",
"group_albums_by": "Agrupa àlbums per...",
"group_country": "Agrupar per país",
@@ -1262,6 +1271,7 @@
"main_branch_warning": "Esteu utilitzant una versió en desenvolupament; Recomanem fer servir una versió publicada!",
"main_menu": "Menú principal",
"make": "Fabricant",
"manage_geolocation": "Gestioneu la vostra ubicació",
"manage_shared_links": "Administrar enllaços compartits",
"manage_sharing_with_partners": "Gestiona la compartició amb els companys",
"manage_the_app_settings": "Gestioneu la configuració de l'aplicació",
@@ -1508,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
"profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Manera de només lectura activada. Feu doble click a la icona de l'avatar de l'usuari per sortir.",
"profile_drawer_server_out_of_date_major": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió major.",
"profile_drawer_server_out_of_date_minor": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió menor.",
"profile_image_of_user": "Imatge de perfil de {user}",
@@ -1553,6 +1564,8 @@
"rating_description": "Mostrar la valoració EXIF al panell d'informació",
"reaction_options": "Opcions de reacció",
"read_changelog": "Llegeix el registre de canvis",
"readonly_mode_disabled": "Mode de només lectura desactivat",
"readonly_mode_enabled": "Mode de només lectura activat",
"reassign": "Reassignar",
"reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova",
@@ -1722,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "Error al crear l'àlbum",
"selected": "Seleccionat",
"selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}",
"selected_gps_coordinates": "seleccio de coordinades GPS",
"send_message": "Envia missatge",
"send_welcome_email": "Envia correu de benvinguda",
"server_endpoint": "Endpoint de Servidor",
@@ -1832,8 +1846,10 @@
"shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment",
"show_album_options": "Mostra les opcions d'àlbum",
"show_albums": "Mostrar àlbums",
"show_all_assets": "Mostrar tots els elements",
"show_all_people": "Veure totes les persones",
"show_and_hide_people": "Mostra i amaga persones",
"show_assets_without_location": "Mostra els elements sense ubicació",
"show_file_location": "Mostra l'ubicació del fitxer",
"show_gallery": "Mostra la galeria",
"show_hidden_people": "Mostra persones ocultes",
@@ -1941,7 +1957,9 @@
"to_change_password": "Canviar la contrasenya",
"to_favorite": "Prefereix",
"to_login": "Iniciar sessió",
"to_multi_select": "per multi-seleccionar",
"to_parent": "Anar als pares",
"to_select": "per seleccionar",
"to_trash": "Paperera",
"toggle_settings": "Canvia configuració",
"total": "Total",
@@ -1991,6 +2009,7 @@
"unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}",
"untagged": "Sense etiqueta",
"up_next": "Pròxim",
"update_location_action_prompt": "Actualitza la ubicació de {count} elements seleccionats amb:",
"updated_at": "Actualitzat",
"updated_password": "Contrasenya actualitzada",
"upload": "Pujar",
@@ -2015,6 +2034,7 @@
"use_biometric": "Empra biometria",
"use_current_connection": "utilitzar la connexió actual",
"use_custom_date_range": "Fes servir un rang de dates personalitzat",
"use_this_location": "Fes clic per utilitzar la ubicació",
"user": "Usuari",
"user_has_been_deleted": "Aquest usuari ha sigut eliminat.",
"user_id": "ID d'usuari",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Otevřít vyhledávací filtry",
"options": "Možnosti",
"or": "nebo",
"organize_into_albums": "Organizovat do alb",
"organize_into_albums_description": "Umístit existující fotky do alb s použitím aktuálního nastavení synchronizace",
"organize_your_library": "Uspořádejte si knihovnu",
"original": "originál",
"other": "Ostatní",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Stav podporovatele",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktový klíč serveru spravuje správce",
"query_asset_id": "ID položky dotazu",
"queue_status": "Ve frontě {count}/{total}",
"rating": "Hodnocení hvězdičkami",
"rating_clear": "Vyčistit hodnocení",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album",
"selected": "Vybráno",
"selected_count": "{count, plural, one {# vybraný} few {# vybrané} other {# vybraných}}",
"selected_gps_coordinates": "vybrané GPS souřadnice",
"selected_gps_coordinates": "Vybrané GPS souřadnice",
"send_message": "Odeslat zprávu",
"send_welcome_email": "Poslat uvítací e-mail",
"server_endpoint": "Koncový bod serveru",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Zobrazit další položku",
"view_previous_asset": "Zobrazit předchozí položku",
"view_qr_code": "Zobrazit QR kód",
"view_similar_photos": "Zobrazit podobné fotky",
"view_stack": "Zobrazit seskupení",
"view_user": "Zobrazit uživatele",
"viewer_remove_from_stack": "Odstranit ze zásobníku",

View File

@@ -126,13 +126,13 @@
"machine_learning_clip_model": "CLIP-model",
"machine_learning_clip_model_description": "Navnet på CLIP-modellen på listen <link>her</link>. Bemærk at du skal genkøre \"Smart Søgning\"-jobbet for alle billeder, hvis du skifter model.",
"machine_learning_duplicate_detection": "Dubletdetektion",
"machine_learning_duplicate_detection_enabled": "Aktiver duplikatdetektion",
"machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler blive de-duplikerede.",
"machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige duplikater",
"machine_learning_duplicate_detection_enabled": "Aktiver dubletdetektion",
"machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler stadig blive de-duplikerede.",
"machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige dubletter",
"machine_learning_enabled": "Aktivér maskinlæring",
"machine_learning_enabled_description": "Hvis deaktiveret, vil alle ML-funktioner blive deaktiveret uanset nedenstående indstillinger.",
"machine_learning_facial_recognition": "Ansigtsgenkendelse",
"machine_learning_facial_recognition_description": "Registrer, genkend og grupper ansigter i billeder",
"machine_learning_facial_recognition_description": "Opdag, genkend og gruppér ansigter i billeder",
"machine_learning_facial_recognition_model": "Ansigtsgenkendelsesmodel",
"machine_learning_facial_recognition_model_description": "Modellerne er listet i faldende størrelsesorden. Større modeller er langsommere og bruger mere hukommelse, men giver bedre resultater. Bemærk, at du skal køre ansigtsopdagelsesopgaven igen for alle billeder, når du ændrer en model.",
"machine_learning_facial_recognition_setting": "Aktivér ansigtgenkendelse",
@@ -221,6 +221,8 @@
"oauth_mobile_redirect_uri": "Mobilomdiregerings-URL",
"oauth_mobile_redirect_uri_override": "Tilsidesættelse af mobil omdiregerings-URL",
"oauth_mobile_redirect_uri_override_description": "Aktiver, når OAuth-udbyderen ikke tillader en mobil URI, som ''{callback}''",
"oauth_role_claim": "Rolle attribut",
"oauth_role_claim_description": "Tildel automatisk admin adgang på basis af forekomst af denne påstand. Dén kan være enten 'user' eller 'admin'.",
"oauth_settings": "OAuth",
"oauth_settings_description": "Administrer OAuth login-indstillinger",
"oauth_settings_more_details": "Læs flere detaljer om funktionen i <link>dokumentationen</link>.",
@@ -269,6 +271,7 @@
"storage_template_migration_info": "Lager-skabelonen vil konvertere alle filendelser til små bogstaver. Skabelonændringer vil kun gælde for nye mediefiler. For at anvende skabelonen retroaktivt på tidligere uploadede mediefiler skal du køre <link>{job}</link>.",
"storage_template_migration_job": "Lager Skabelon Migreringsjob",
"storage_template_more_details": "For flere detaljer om denne funktion, referer til <template-link>Lager Skabelonen</template-link> og dens <implications-link>implikationer</implications-link>",
"storage_template_onboarding_description_v2": "Når aktiveret, så vil denne funktion auto-organisere filer på grundlag af en brugerdefineret skabelon. For nærmere, se <link>dokumentation</link>.",
"storage_template_path_length": "Anslået sti-længde begrænsning <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "Lagringsskabelon",
"storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil",
@@ -355,7 +358,9 @@
"trash_number_of_days_description": "Antal dage aktiver i skraldespanden skal beholdes inden de fjernes permanent",
"trash_settings": "Skraldeindstillinger",
"trash_settings_description": "Administrér skraldeindstillinger",
"unlink_all_oauth_accounts": "Ophæv link til alle OAuth konti",
"unlink_all_oauth_accounts_description": "Husk at fjerne linket til alle OAuth konti før du migrerer til en ny udbyder.",
"unlink_all_oauth_accounts_prompt": "Er du sikker på, at du vil ophæve link til alle OAuth konti? Dette vil nulstille OAuth ID for hver bruger og kan ikke fortrydes.",
"user_cleanup_job": "Bruger-oprydning",
"user_delete_delay": "<b>{user}</b>'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.",
"user_delete_delay_settings": "Slet forsinkelse",
@@ -390,7 +395,9 @@
"advanced_settings_prefer_remote_subtitle": "Nogle enheder er meget lang tid om at indlæse miniaturebilleder af lokale elementer. Aktiver denne indstilling for at indlæse elementer fra serveren i stedet.",
"advanced_settings_prefer_remote_title": "Foretræk elementer på serveren",
"advanced_settings_proxy_headers_subtitle": "Definer proxy headers Immich skal sende med hver netværks forespørgsel",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_proxy_headers_title": "Proxy headere",
"advanced_settings_readonly_mode_subtitle": "Aktiverer skrivebeskyttet tilstand, hvor billederne alene kan vises. Ting som at vælge flere billeder, dele, caste og slette er alle deaktiveret. Aktiver skrivebeskyttet tilstand via en bruger avatar fra hovedskærmen",
"advanced_settings_readonly_mode_title": "Skrivebeskyttet tilstand",
"advanced_settings_self_signed_ssl_subtitle": "Spring verificering af SSL-certifikat over for serverens endelokation. Kræves for selvsignerede certifikater.",
"advanced_settings_self_signed_ssl_title": "Tillad selvsignerede certifikater",
"advanced_settings_sync_remote_deletions_subtitle": "Slet eller gendan automatisk en mediefil på denne enhed, når denne handling foretages på Immich webinterface",
@@ -406,6 +413,7 @@
"album_cover_updated": "Albumcover opdateret",
"album_delete_confirmation": "Er du sikker på at du vil slette albummet {album}?",
"album_delete_confirmation_description": "Hvis dette album er delt, vil andre brugere ikke længere kunne få adgang til det.",
"album_deleted": "Album slettet",
"album_info_card_backup_album_excluded": "EKSKLUDERET",
"album_info_card_backup_album_included": "INKLUDERET",
"album_info_updated": "Albuminfo opdateret",
@@ -415,6 +423,7 @@
"album_options": "Albumindstillinger",
"album_remove_user": "Fjern bruger?",
"album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?",
"album_search_not_found": "Ingen album fundet som matcher din søgning",
"album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.",
"album_updated": "Album opdateret",
"album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler",
@@ -434,6 +443,7 @@
"albums_default_sort_order": "Standard album sortering",
"albums_default_sort_order_description": "Grundlæggende sortering ved oprettelse af nyt album.",
"albums_feature_description": "Samling af billeder der kan deles med andre brugere.",
"albums_on_device_count": "Albummer på enheden ({count})",
"all": "Alt",
"all_albums": "Alle albummer",
"all_people": "Alle personer",
@@ -453,7 +463,9 @@
"app_bar_signout_dialog_title": "Log ud",
"app_settings": "Appindstillinger",
"appears_in": "Optræder i",
"apply_count": "Brug ({count, number})",
"archive": "Arkiv",
"archive_action_prompt": "{count} føjet til arkiv",
"archive_or_unarchive_photo": "Arkivér eller dearkivér billede",
"archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
"archive_page_title": "Arkivér ({count})",
@@ -488,10 +500,12 @@
"asset_uploading": "Uploader…",
"asset_viewer_settings_subtitle": "Administrer indstillinger for gallerifremviser",
"asset_viewer_settings_title": "Billedviser",
"assets": "elementer",
"assets": "Objekter",
"assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}",
"assets_added_to_album_count": "{count, plural, one {# mediefil} other {# mediefiler}} tilføjet til albummet",
"assets_added_to_albums_count": "Tilføjet {assetTotal, plural, one {# asset} other {# assets}} til {albumTotal, plural, one {# album} other {# albums}}",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Billed} other {Billeder}} kan ikke blive tilføjet til album",
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} kan ikke føjes til i nogen af albummerne",
"assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}",
"assets_deleted_permanently": "{count} element(er) blev fjernet permanent",
"assets_deleted_permanently_from_server": "{count} element(er) blev fjernet permanent fra Immich serveren",
@@ -508,6 +522,7 @@
"assets_trashed_count": "{count, plural, one {# mediefil} other {# mediefiler}} smidt i papirkurven",
"assets_trashed_from_server": "{count} element(er) blev smidt i Immich serverens papirkurv",
"assets_were_part_of_album_count": "mediefil{count, plural, one {mediefil} other {mediefiler}} er allerede en del af albummet",
"assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} er allerede en del af albummerne",
"authorized_devices": "Tilladte enheder",
"automatic_endpoint_switching_subtitle": "Forbind lokalt over det anviste WiFi, når det er tilgængeligt og brug alternative forbindelser andre stæder",
"automatic_endpoint_switching_title": "Automatisk skift af URL",
@@ -572,14 +587,18 @@
"backup_controller_page_turn_on": "Slå sikkerhedskopiering til",
"backup_controller_page_uploading_file_info": "Uploader filinformation",
"backup_err_only_album": "Kan ikke slette det eneste album",
"backup_info_card_assets": "elementer",
"backup_info_card_assets": "objekter",
"backup_manual_cancelled": "Annulleret",
"backup_manual_in_progress": "Upload er allerede undervejs. Prøv igen efter noget tid",
"backup_manual_success": "Succes",
"backup_manual_title": "Uploadstatus",
"backup_options": "Backup indstillinger",
"backup_options_page_title": "Backupindstillinger",
"backup_setting_subtitle": "Administrer indstillnger for upload i forgrund og baggrund",
"backup_settings_subtitle": "Håndtere upload indstillinger",
"backward": "Baglæns",
"beta_sync": "Beta synkroniseringsstatus",
"beta_sync_subtitle": "Håndter det nye synkroniseringssystem",
"biometric_auth_enabled": "Biometrisk adgangskontrol slået til",
"biometric_locked_out": "Du er låst ude af biometrisk adgangskontrol",
"biometric_no_options": "Ingen biometrisk adgangskontrol tilgængelig",
@@ -614,6 +633,7 @@
"cancel": "Annullér",
"cancel_search": "Annullér søgning",
"canceled": "Annulleret",
"canceling": "Annullerer",
"cannot_merge_people": "Kan ikke sammenflette personer",
"cannot_undo_this_action": "Du kan ikke fortryde denne handling!",
"cannot_update_the_description": "Kan ikke opdatere beskrivelsen",
@@ -645,6 +665,7 @@
"clear": "Ryd",
"clear_all": "Ryd alle",
"clear_all_recent_searches": "Ryd alle seneste søgninger",
"clear_file_cache": "Ryd filcache",
"clear_message": "Ryd bedsked",
"clear_value": "Ryd værdi",
"client_cert_dialog_msg_confirm": "OK",
@@ -715,6 +736,7 @@
"create_new_user": "Opret ny bruger",
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
"create_shared_album_page_share_select_photos": "Vælg Billeder",
"create_shared_link": "Opret delt link",
"create_tag": "Opret tag",
"create_tag_description": "Opret et nyt tag. For indlejrede tags skal du indtaste den fulde sti til tagget inklusive skråstreger.",
"create_user": "Opret bruger",
@@ -727,9 +749,11 @@
"current_server_address": "Nuværende serveraddresse",
"custom_locale": "Brugerdefineret lokale",
"custom_locale_description": "Formatér datoer og tal baseret på sproget og regionen",
"custom_url": "Tilpasset URL",
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"dark": "Mørk",
"dark_theme": "Skift til mørkt tema",
"date_after": "Dato efter",
"date_and_time": "Dato og klokkeslæt",
"date_before": "Dato før",
@@ -737,6 +761,7 @@
"date_of_birth_saved": "Fødselsdatoen blev gemt korrekt",
"date_range": "Datointerval",
"day": "Dag",
"days": "Dage",
"deduplicate_all": "Kopier alle",
"deduplication_criteria_1": "Billedstørrelse i bytes",
"deduplication_criteria_2": "Antal EXIF-data",
@@ -745,6 +770,8 @@
"default_locale": "Standardlokalitet",
"default_locale_description": "Formatér datoer og tal baseret på din browsers regions indstillinger",
"delete": "Slet",
"delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt",
"delete_action_prompt": "{count} slettet",
"delete_album": "Slet album",
"delete_api_key_prompt": "Er du sikker på, at du vil slette denne API-nøgle?",
"delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
@@ -758,9 +785,12 @@
"delete_key": "Slet nøgle",
"delete_library": "Slet bibliotek",
"delete_link": "Slet link",
"delete_local_action_prompt": "{count} slettet lokalt",
"delete_local_dialog_ok_backed_up_only": "Slet kun backup",
"delete_local_dialog_ok_force": "Slet alligevel",
"delete_others": "Slet andre",
"delete_permanently": "Slet permanent",
"delete_permanently_action_prompt": "{count} slettet permanent",
"delete_shared_link": "Slet delt link",
"delete_shared_link_dialog_title": "Slet delt link",
"delete_tag": "Slet tag",
@@ -771,6 +801,7 @@
"description": "Beskrivelse",
"description_input_hint_text": "Tilføj en beskrivelse...",
"description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer",
"deselect_all": "Afmarkér alt",
"details": "DETALJER",
"direction": "Retning",
"disabled": "Deaktiveret",
@@ -788,6 +819,7 @@
"documentation": "Dokumentation",
"done": "Færdig",
"download": "Hent",
"download_action_prompt": "Downloader {count} objekter",
"download_canceled": "Download annulleret",
"download_complete": "Download fuldført",
"download_enqueue": "Donload sat i kø",
@@ -814,8 +846,12 @@
"edit": "Rediger",
"edit_album": "Redigér album",
"edit_avatar": "Redigér avatar",
"edit_birthday": "Rediger fødselsdag",
"edit_date": "Redigér dato",
"edit_date_and_time": "Redigér dato og tid",
"edit_date_and_time_action_prompt": "{count} dato og tid redigeret",
"edit_date_and_time_by_offset": "Forskyde dato med offset",
"edit_date_and_time_by_offset_interval": "Nyt datointerval: {from} - {to}",
"edit_description": "Rediger beskrivelse",
"edit_description_prompt": "Vælg venligst en ny beskrivelse:",
"edit_exclusion_pattern": "Redigér udelukkelsesmønster",
@@ -825,6 +861,7 @@
"edit_key": "Redigér nøgle",
"edit_link": "Rediger link",
"edit_location": "Rediger placering",
"edit_location_action_prompt": "{count} geolokation redigeret",
"edit_location_dialog_title": "Placering",
"edit_name": "Rediger navn",
"edit_people": "Redigér personer",
@@ -843,6 +880,7 @@
"empty_trash": "Tøm papirkurv",
"empty_trash_confirmation": "Er du sikker på, at du vil tømme papirkurven? Dette vil fjerne alle objekter i papirkurven permanent fra Immich.\nDu kan ikke fortryde denne handling!",
"enable": "Aktivér",
"enable_backup": "Aktiver backup",
"enable_biometric_auth_description": "Indtast din PIN kode for at slå biometrisk adgangskontrol til",
"enabled": "Aktiveret",
"end_date": "Slutdato",
@@ -886,6 +924,7 @@
"failed_to_load_notifications": "Kunne ikke indlæse notifikationer",
"failed_to_load_people": "Indlæsning af personer mislykkedes",
"failed_to_remove_product_key": "Fjernelse af produktnøgle mislykkedes",
"failed_to_reset_pin_code": "Kunne ikke resette PIN-koden",
"failed_to_stack_assets": "Det lykkedes ikke at stable mediefiler",
"failed_to_unstack_assets": "Det lykkedes ikke at fjerne gruperingen af mediefiler",
"failed_to_update_notification_status": "Kunne ikke uploade notifikations status",
@@ -894,6 +933,7 @@
"paths_validation_failed": "{paths, plural, one {# sti} other {# stier}} slog fejl ved validering",
"profile_picture_transparent_pixels": "Profilbilleder kan ikke have gennemsigtige pixels. Zoom venligst ind og/eller flyt billedet.",
"quota_higher_than_disk_size": "Du har sat en kvote der er større end disken",
"something_went_wrong": "Noget gik galt",
"unable_to_add_album_users": "Ikke i stand til at tilføje brugere til album",
"unable_to_add_assets_to_shared_link": "Kan ikke tilføje mediefiler til det delte link",
"unable_to_add_comment": "Ikke i stand til at tilføje kommentar",
@@ -979,6 +1019,7 @@
},
"exif": "Exif",
"exif_bottom_sheet_description": "Tilføj beskrivelse...",
"exif_bottom_sheet_description_error": "Fejl ved opdatering af beskrivelsen",
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "LOKATION",
"exif_bottom_sheet_people": "PERSONER",
@@ -996,6 +1037,8 @@
"explorer": "Udforske",
"export": "Eksportér",
"export_as_json": "Eksportér som JSON",
"export_database": "Eksporter database",
"export_database_description": "Eksporter SQLite databasen",
"extension": "Udvidelse",
"external": "Ekstern",
"external_libraries": "Eksterne biblioteker",
@@ -1007,6 +1050,7 @@
"failed_to_load_assets": "Kunne ikke indlæse mediefiler",
"failed_to_load_folder": "Kunne ikke indlæse mappe",
"favorite": "Favorit",
"favorite_action_prompt": "{count} føjet til favoritter",
"favorite_or_unfavorite_photo": "Tilføj eller fjern fra yndlingsbilleder",
"favorites": "Favoritter",
"favorites_page_no_favorites": "Ingen favoritter blev fundet",
@@ -1021,21 +1065,29 @@
"filter_people": "Filtrér personer",
"filter_places": "Filtrer steder",
"find_them_fast": "Find dem hurtigt med søgning via navn",
"first": "Første",
"fix_incorrect_match": "Fix forkert match",
"folder": "Mappe",
"folder_not_found": "Mappe ikke fundet",
"folders": "Mapper",
"folders_feature_description": "Gennemse mappevisningen efter fotos og videoer på filsystemet",
"forgot_pin_code_question": "Har du glemt PIN-koden?",
"forward": "Fremad",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Denne funktion indlæser eksterne ressourcer fra Google for at virke.",
"general": "Generel",
"geolocation_instruction_all_have_location": "Alle objekter for denne dato har allerede lokations-data. Prøv at vise alle objekter eller vælg en anden dato",
"geolocation_instruction_location": "Klik på et objekt med GPS-koordinater for at bruge dettes position, eller vælg position direkte på kortet",
"geolocation_instruction_no_date": "Vælg en dato, for at administrere lokationsdata på billeder og videoer fra den dag",
"geolocation_instruction_no_photos": "Ingen fotos eller videoer fundet for den dato. Vælg en anden dato for at vise dem",
"get_help": "Få hjælp",
"get_wifiname_error": "Kunne ikke hente Wi-Fi-navn. Sørg for, at du har givet de nødvendige tilladelser og er forbundet til et Wi-Fi-netværk",
"getting_started": "Kom godt i gang",
"go_back": "Gå tilbage",
"go_to_folder": "Gå til mappe",
"go_to_search": "Gå til søgning",
"gps": "GPS",
"gps_missing": "Ingen GPS",
"grant_permission": "Giv tilladelse",
"group_albums_by": "Gruppér albummer efter...",
"group_country": "Gruppér efter land",
@@ -1046,6 +1098,9 @@
"haptic_feedback_switch": "Slå haptisk feedback til",
"haptic_feedback_title": "Haptisk feedback",
"has_quota": "Har kvote",
"hash_asset": "Hash objekter",
"hashed_assets": "Hashede objekter",
"hashing": "Hasher",
"header_settings_add_header_tip": "Tilføj Header",
"header_settings_field_validator_msg": "Værdi kan ikke være tom",
"header_settings_header_name_input": "Header navn",
@@ -1077,7 +1132,9 @@
"home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over",
"host": "Host",
"hour": "Time",
"hours": "Timer",
"id": "ID",
"idle": "Inaktiv",
"ignore_icloud_photos": "Ignorer iCloud-billeder",
"ignore_icloud_photos_description": "Billeder der er gemt på iCloud vil ikke blive uploadet til Immich-serveren",
"image": "Billede",
@@ -1135,10 +1192,13 @@
"language_no_results_title": "Ingen sprog fundet",
"language_search_hint": "Vælg sprog...",
"language_setting_description": "Vælg dit foretrukne sprog",
"large_files": "Store filer",
"last": "Sidste",
"last_seen": "Sidst set",
"latest_version": "Seneste version",
"latitude": "Breddegrad",
"leave": "Forlad",
"leave_album": "Forlad album",
"lens_model": "Objektivmodel",
"let_others_respond": "Lad andre svare",
"level": "Niveau",
@@ -1150,7 +1210,9 @@
"library_page_sort_created": "Senest oprettet",
"library_page_sort_last_modified": "Sidst redigeret",
"library_page_sort_title": "Albumtitel",
"licenses": "Licenser",
"light": "Lys",
"like": "Synes om",
"like_deleted": "Ligesom slettet",
"link_motion_video": "Link bevægelsesvideo",
"link_to_oauth": "Link til OAuth",
@@ -1158,7 +1220,9 @@
"list": "Liste",
"loading": "Indlæser",
"loading_search_results_failed": "Indlæsning af søgeresultater fejlede",
"local": "Lokal",
"local_asset_cast_failed": "Kan ikke caste et aktiv, der ikke er uploadet til serveren",
"local_assets": "Lokale objekter",
"local_network": "Lokalt netværk",
"local_network_sheet_info": "Appen vil oprette forbindelse til serveren via denne URL, når du bruger det angivne WiFi-netværk",
"location_permission": "Tilladelse til placering",
@@ -1172,6 +1236,7 @@
"locked_folder": "Låst mappe",
"log_out": "Log ud",
"log_out_all_devices": "Log ud af alle enheder",
"logged_in_as": "Logget ind som {user}",
"logged_out_all_devices": "Logget ud af alle enheder",
"logged_out_device": "Logget ud af enhed",
"login": "Log ind",
@@ -1180,7 +1245,7 @@
"login_form_back_button_text": "Tilbage",
"login_form_email_hint": "din-e-mail@e-mail.com",
"login_form_endpoint_hint": "http://din-server-ip:port",
"login_form_endpoint_url": "Server Endpoint URL",
"login_form_endpoint_url": "Server endepunkt URL",
"login_form_err_http": "Angiv venligst http:// eller https://",
"login_form_err_invalid_email": "Ugyldig e-mail",
"login_form_err_invalid_url": "Ugyldig webadresse",
@@ -1206,6 +1271,7 @@
"main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!",
"main_menu": "Hovedmenu",
"make": "Producent",
"manage_geolocation": "Administrer placering",
"manage_shared_links": "Håndter delte links",
"manage_sharing_with_partners": "Administrér deling med partnere",
"manage_the_app_settings": "Administrer appindstillinger",
@@ -1258,6 +1324,7 @@
"merged_people_count": "{count, plural, one {# person} other {# personer}} lagt sammen",
"minimize": "Minimér",
"minute": "Minut",
"minutes": "Minutter",
"missing": "Mangler",
"model": "Model",
"month": "Måned",
@@ -1265,6 +1332,7 @@
"more": "Mere",
"move": "Flyt",
"move_off_locked_folder": "Flyt ud af låst mappe",
"move_to_lock_folder_action_prompt": "{count} føjet til i den låste mappe",
"move_to_locked_folder": "Flyt til låst mappe",
"move_to_locked_folder_confirmation": "Disse billeder og videoer vil blive fjernet fra alle albums, og vil kun være synlig fra den låste mappe",
"moved_to_archive": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til arkivet",
@@ -1276,6 +1344,9 @@
"my_albums": "Mine albummer",
"name": "Navn",
"name_or_nickname": "Navn eller kælenavn",
"network_requirement_photos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine fotos",
"network_requirement_videos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine videoer",
"network_requirements_updated": "Netværkskravene er ændret, backup-køen nulstilles",
"networking_settings": "Netværk",
"networking_subtitle": "Administrer serverens endepunktindstillinger",
"never": "aldrig",
@@ -1311,6 +1382,7 @@
"no_results": "Ingen resultater",
"no_results_description": "Prøv et synonym eller et mere generelt søgeord",
"no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netværk",
"no_uploads_in_progress": "Ingen upload i gang",
"not_in_any_album": "Ikke i noget album",
"not_selected": "Ikke valgt",
"note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør",
@@ -1326,6 +1398,7 @@
"oauth": "OAuth",
"official_immich_resources": "Officielle Immich-ressourcer",
"offline": "Offline",
"offset": "Forskydning",
"ok": "Ok",
"oldest_first": "Ældste først",
"on_this_device": "På denne enhed",
@@ -1348,6 +1421,7 @@
"original": "original",
"other": "Andet",
"other_devices": "Andre enheder",
"other_entities": "Andre enheder",
"other_variables": "Andre variable",
"owned": "Egne",
"owner": "Ejer",
@@ -1402,6 +1476,9 @@
"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.",
"person": "Person",
"person_age_months": "{months, plural, one {# month} other {# months}} gammel",
"person_age_year_months": "1 år, {months, plural, one {# month} other {# months}} gammel",
"person_age_years": "{years, plural, other {# years}} gammel",
"person_birthdate": "Født den {date}",
"person_hidden": "{name}{hidden, select, true { (skjult)} other {}}",
"photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller også har du ikke nogen bruger at dele med.",
@@ -1441,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "Mobilapp er forældet. Opdater venligst til den nyeste mindre version.",
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Skrivebeskyttet tilstand aktiveret. Dobbeltklik på bruger avatar ikonet for at afslutte.",
"profile_drawer_server_out_of_date_major": "Server er forældet. Opdater venligst til den nyeste større version.",
"profile_drawer_server_out_of_date_minor": "Server er forældet. Opdater venligst til den nyeste mindre version.",
"profile_image_of_user": "Profilbillede af {user}",
@@ -1476,15 +1554,18 @@
"purchase_remove_server_product_key": "Fjern serverens produktnøgle",
"purchase_remove_server_product_key_prompt": "Er du sikker på, at du vil fjerne serverproduktnøglen?",
"purchase_server_description_1": "For hele serveren",
"purchase_server_description_2": "Supporter status",
"purchase_server_description_2": "Supporterstatus",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren",
"queue_status": "Kø {count}/{total}",
"rating": "Stjernebedømmelse",
"rating_clear": "Nulstil vurdering",
"rating_count": "{count, plural, one {# stjerne} other {# stjerner}}",
"rating_description": "Vis EXIF-klassificeringen i infopanelet",
"reaction_options": "Reaktionsindstillinger",
"read_changelog": "Læs ændringslog",
"readonly_mode_disabled": "Skrivebeskyttet tilstand deaktiveret",
"readonly_mode_enabled": "Skrivebeskyttet tilstand aktiveret",
"reassign": "Gentildel",
"reassigned_assets_to_existing_person": "{count, plural, one {# mediefil} other {# mediefiler}} er blevet gentildelt til {name, select, null {en eksisterende person} other {{name}}}",
"reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person",
@@ -1507,6 +1588,8 @@
"refreshing_faces": "Opdaterer ansigter",
"refreshing_metadata": "Opdaterer metadata",
"regenerating_thumbnails": "Regenererer forhåndsvisninger",
"remote": "Eksternt",
"remote_assets": "Eksterne objekter",
"remove": "Fjern",
"remove_assets_album_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra albummet?",
"remove_assets_shared_link_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra dette delte link?",
@@ -1514,7 +1597,9 @@
"remove_custom_date_range": "Fjern tilpasset datointerval",
"remove_deleted_assets": "Fjern slettede mediefiler",
"remove_from_album": "Fjern fra album",
"remove_from_album_action_prompt": "{count} fjernet fra albummet",
"remove_from_favorites": "Fjern fra favoritter",
"remove_from_lock_folder_action_prompt": "{count} fjernet fra den låste mappe",
"remove_from_locked_folder": "Fjern fra låst mappe",
"remove_from_locked_folder_confirmation": "Er du sikker på at du vil flytte disse billeder og videoer ud af den låste mappe? De vil være synlige i dit bibliotek.",
"remove_from_shared_link": "Fjern fra delt link",
@@ -1542,19 +1627,28 @@
"reset_password": "Nulstil adgangskode",
"reset_people_visibility": "Nulstil personsynlighed",
"reset_pin_code": "Nulstil PIN kode",
"reset_pin_code_description": "Hvis du har glemt din PIN-kode, kan du kontakte serveradministratoren for at få den stillet tilbage",
"reset_pin_code_success": "PIN-koden er stillet tilbage",
"reset_pin_code_with_password": "Du kan altid nulstille din PIN-kode med dit password",
"reset_sqlite": "Reset SQLite Databasen",
"reset_sqlite_confirmation": "Er du sikker på, at du vil nulstille SQLite databasen? Du er nødt til at logge ud og ind igen for at gensynkronisere dine data",
"reset_sqlite_success": "Vellykket reset af SQLite databasen",
"reset_to_default": "Nulstil til standard",
"resolve_duplicates": "Løs dubletter",
"resolved_all_duplicates": "Alle dubletter løst",
"restore": "Gendan",
"restore_all": "Gendan alle",
"restore_trash_action_prompt": "{count} genskabt fra papirkurven",
"restore_user": "Gendan bruger",
"restored_asset": "Gendannet mediefilen",
"resume": "Genoptag",
"retry_upload": "Forsøg upload igen",
"review_duplicates": "Gennemgå dubletter",
"review_large_files": "Gennemgå store filer",
"role": "Rolle",
"role_editor": "Redaktør",
"role_viewer": "Seer",
"running": "Kører",
"save": "Gem",
"save_to_gallery": "Gem til galleri",
"saved_api_key": "Gemt API-nøgle",
@@ -1627,6 +1721,7 @@
"select_album_cover": "Vælg albumcover",
"select_all": "Vælg alle",
"select_all_duplicates": "Vælg alle dubletter",
"select_all_in": "Vælg alt i {group}",
"select_avatar_color": "Vælg avatarfarve",
"select_face": "Vælg ansigt",
"select_featured_photo": "Vælg forsidebillede",
@@ -1640,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
"selected": "Valgt",
"selected_count": "{count, plural, one {# valgt} other {# valgte}}",
"selected_gps_coordinates": "Valgte GPS-koordinater",
"send_message": "Send besked",
"send_welcome_email": "Send velkomstemail",
"server_endpoint": "Server endepunkt",
@@ -1677,7 +1773,7 @@
"setting_notifications_subtitle": "Tilpas dine notifikationspræferencer",
"setting_notifications_total_progress_subtitle": "Samlet uploadstatus (færdige/samlet antal elementer)",
"setting_notifications_total_progress_title": "Vis samlet baggrundsuploadstatus",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_looping_title": "Looper",
"setting_video_viewer_original_video_subtitle": "Når der streames video fra serveren, afspil da den originale selv når en omkodet udgave er tilgængelig. Kan føre til buffering. Videoer, der er tilgængelige lokalt, afspilles i original kvalitet uanset denne indstilling.",
"setting_video_viewer_original_video_title": "Tving original video",
"settings": "Indstillinger",
@@ -1685,6 +1781,7 @@
"settings_saved": "Indstillinger er gemt",
"setup_pin_code": "Sæt in PIN kode",
"share": "Del",
"share_action_prompt": "Delte {count} objekter",
"share_add_photos": "Tilføj billeder",
"share_assets_selected": "{count} valgt",
"share_dialog_preparing": "Forbereder...",
@@ -1706,6 +1803,7 @@
"shared_link_clipboard_copied_massage": "Kopieret til udklipsholderen",
"shared_link_clipboard_text": "Link: {link}\nAdgangskode: {password}",
"shared_link_create_error": "Der opstod en fejl i oprettelsen af et delt link",
"shared_link_custom_url_description": "Adgang til dette delte link med en selvdefineret URL",
"shared_link_edit_description_hint": "Indtast beskrivelse",
"shared_link_edit_expire_after_option_day": "1 dag",
"shared_link_edit_expire_after_option_days": "{count} dage",
@@ -1731,6 +1829,7 @@
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Håndter delte links",
"shared_link_options": "Muligheder for delt link",
"shared_link_password_description": "Kræv et kodeord for at få adgang til dette delte link",
"shared_links": "Delte links",
"shared_links_description": "Del billeder og videoer med et link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}",
@@ -1747,8 +1846,10 @@
"shift_to_permanent_delete": "tryk på ⇧ for at slette aktiv permanent",
"show_album_options": "Vis albumindstillinger",
"show_albums": "Vis albummer",
"show_all_assets": "Vis alle objekter",
"show_all_people": "Vis alle personer",
"show_and_hide_people": "Vis & skjul personer",
"show_assets_without_location": "Vis objekter uden lokation",
"show_file_location": "Vis filplacering",
"show_gallery": "Vis galleri",
"show_hidden_people": "Vis skjulte personer",
@@ -1780,12 +1881,14 @@
"sort_created": "Dato oprettet",
"sort_items": "Antal genstande",
"sort_modified": "Ændret dato",
"sort_newest": "Nyeste foto",
"sort_oldest": "Ældste foto",
"sort_people_by_similarity": "Sorter efter personer der ligner hinanden",
"sort_recent": "Seneste foto",
"sort_title": "Titel",
"source": "Kilde",
"stack": "Stak",
"stack_action_prompt": "{count} stakket",
"stack_duplicates": "Stak dubletter",
"stack_select_one_photo": "Vælg ét hovedbillede til stakken",
"stack_selected_photos": "Stak valgte billeder",
@@ -1805,6 +1908,7 @@
"storage_quota": "Lagringskvota",
"storage_usage": "{used} ud af {available} brugt",
"submit": "Indsend",
"success": "Vellykket",
"suggestions": "Anbefalinger",
"sunrise_on_the_beach": "Solopgang på stranden",
"support": "Support",
@@ -1814,6 +1918,8 @@
"sync": "Synkronisér",
"sync_albums": "Synkroniser albummer",
"sync_albums_manual_subtitle": "Synkroniser alle uploadet billeder og videoer til de valgte backupalbummer",
"sync_local": "Synkroniser lokalt",
"sync_remote": "Synkroniser eksternt",
"sync_upload_album_setting_subtitle": "Opret og upload dine billeder og videoer til de valgte albummer i Immich",
"tag": "Tag",
"tag_assets": "Tag mediefiler",
@@ -1824,6 +1930,7 @@
"tag_updated": "Opdateret tag: {tag}",
"tagged_assets": "Tagget {count, plural, one {# aktiv} other {# aktiver}}",
"tags": "Tags",
"tap_to_run_job": "Tryk for at køre jobbet",
"template": "Skabelon",
"theme": "Tema",
"theme_selection": "Temavalg",
@@ -1850,12 +1957,15 @@
"to_change_password": "Skift adgangskode",
"to_favorite": "Gør til favorit",
"to_login": "Login",
"to_multi_select": "For at vælge flere",
"to_parent": "Gå op",
"to_select": "for at vælge",
"to_trash": "Papirkurv",
"toggle_settings": "Slå indstillinger til eller fra",
"total": "Total",
"total_usage": "Samlet forbrug",
"trash": "Papirkurv",
"trash_action_prompt": "{count} flyttet til papirkurven",
"trash_all": "Smid alle ud",
"trash_count": "Slet {count, number}",
"trash_delete_asset": "Flyt mediefil til Papirkurv",
@@ -1873,9 +1983,11 @@
"unable_to_change_pin_code": "Kunne ikke ændre PIN kode",
"unable_to_setup_pin_code": "Kunne ikke sætte PIN kode",
"unarchive": "Afakivér",
"unarchive_action_prompt": "{count} slettet fra Arkiv",
"unarchived_count": "{count, plural, other {Uarkiveret #}}",
"undo": "Fortryd",
"unfavorite": "Fjern favorit",
"unfavorite_action_prompt": "{count} slettet fra Favoritter",
"unhide_person": "Stop med at skjule person",
"unknown": "Ukendt",
"unknown_country": "Ukendt land",
@@ -1891,16 +2003,23 @@
"unsaved_change": "Ændring, der ikke er gemt",
"unselect_all": "Fravælg alle",
"unselect_all_duplicates": "Fjern markeringen af alle dubletter",
"unselect_all_in": "Afmarkér alle i {group}",
"unstack": "Fjern fra stak",
"unstack_action_prompt": "{count} ustakket",
"unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}",
"untagged": "Umærket",
"up_next": "Næste",
"update_location_action_prompt": "Opdater lokationen for {count} valgte objekter med:",
"updated_at": "Opdateret",
"updated_password": "Opdaterede adgangskode",
"upload": "Upload",
"upload_action_prompt": "{count} i kø til upload",
"upload_concurrency": "Upload samtidighed",
"upload_details": "Upload detaljer",
"upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?",
"upload_dialog_title": "Upload element",
"upload_errors": "Upload afsluttet med {count, plural, one {# fejl} other {# fejl}}. Opdater siden for at se nye uploadaktiver.",
"upload_finished": "Upload fuldført",
"upload_progress": "Resterende {remaining, number} - Behandlet {processed, number}/{total, number}",
"upload_skipped_duplicates": "Sprang over {count, plural, one {# duplet aktiv} other {# duplikerede aktiver}}",
"upload_status_duplicates": "Dubletter",
@@ -1909,11 +2028,13 @@
"upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.",
"upload_to_immich": "Upload til Immich ({count})",
"uploading": "Uploader",
"uploading_media": "Uploader media",
"url": "URL",
"usage": "Forbrug",
"use_biometric": "Brug biometrisk",
"use_current_connection": "brug nuværende forbindelse",
"use_custom_date_range": "Brug tilpasset datointerval i stedet",
"use_this_location": "Klik for at benytte lokationen",
"user": "Bruger",
"user_has_been_deleted": "Denne bruger er slettet.",
"user_id": "Bruger-ID",
@@ -1929,6 +2050,7 @@
"user_usage_stats_description": "Vis konto anvendelsesstatistik",
"username": "Brugernavn",
"users": "Brugere",
"users_added_to_album_count": "Føjet {count, plural, one {# bruker} other {# brukere}} til albummet",
"utilities": "Værktøjer",
"validate": "Validér",
"validate_endpoint_error": "Indtast en gyldig URL",
@@ -1947,6 +2069,7 @@
"view_album": "Se album",
"view_all": "Se alle",
"view_all_users": "Se alle brugere",
"view_details": "Vis detaljer",
"view_in_timeline": "Se på tidslinjen",
"view_link": "Vis Link",
"view_links": "Vis links",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Die Suchfilter öffnen",
"options": "Optionen",
"or": "oder",
"organize_into_albums": "In Alben organisieren",
"organize_into_albums_description": "Aktuelle Synchronisationseinstellungen verwenden, um existierende Fotos in Alben zu laden",
"organize_your_library": "Organisiere deine Bibliothek",
"original": "Original",
"other": "Sonstiges",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Unterstützerstatus",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Der Server-Produktschlüssel wird durch den Administrator verwaltet",
"query_asset_id": "Datei-ID abfragen",
"queue_status": "Warteschlange {count}/{total}",
"rating": "Bewertung",
"rating_clear": "Bewertung löschen",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"selected": "Ausgewählt",
"selected_count": "{count, plural, other {# ausgewählt}}",
"selected_gps_coordinates": "Ausgewählte GPS Koordinaten",
"selected_gps_coordinates": "Ausgewählte GPS-Koordinaten",
"send_message": "Nachricht senden",
"send_welcome_email": "Begrüssungsmail senden",
"server_endpoint": "Server-Endpunkt",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Nächste Datei anzeigen",
"view_previous_asset": "Vorherige Datei anzeigen",
"view_qr_code": "QR code anzeigen",
"view_similar_photos": "Zeige ähnliche Fotos an",
"view_stack": "Stapel anzeigen",
"view_user": "Benutzer anzeigen",
"viewer_remove_from_stack": "Aus Stapel entfernen",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Open the search filters",
"options": "Options",
"or": "or",
"organize_into_albums": "Organize into albums",
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
"organize_your_library": "Organize your library",
"original": "original",
"other": "Other",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Supporter status",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "The server product key is managed by the admin",
"query_asset_id": "Query Asset ID",
"queue_status": "Queuing {count}/{total}",
"rating": "Star rating",
"rating_clear": "Clear rating",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Failed to create album",
"selected": "Selected",
"selected_count": "{count, plural, other {# selected}}",
"selected_gps_coordinates": "selected gps coordinates",
"selected_gps_coordinates": "Selected GPS Coordinates",
"send_message": "Send message",
"send_welcome_email": "Send welcome email",
"server_endpoint": "Server Endpoint",
@@ -2077,6 +2080,7 @@
"view_next_asset": "View next asset",
"view_previous_asset": "View previous asset",
"view_qr_code": "View QR code",
"view_similar_photos": "View similar photos",
"view_stack": "View Stack",
"view_user": "View User",
"viewer_remove_from_stack": "Remove from Stack",

View File

@@ -167,8 +167,8 @@
"map_settings": "Mapa",
"map_settings_description": "Administrar la configuración del mapa",
"map_style_description": "Dirección URL a un tema de mapa (style.json)",
"memory_cleanup_job": "Limpieza de memoria",
"memory_generate_job": "Generación de memoria",
"memory_cleanup_job": "Limpieza de recuerdos",
"memory_generate_job": "Generación de recuerdos",
"metadata_extraction_job": "Extracción de metadatos",
"metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras y resolución",
"metadata_faces_import_setting": "Activar importación de caras",
@@ -232,7 +232,7 @@
"oauth_storage_quota_claim_description": "Fijar la cuota de almacenamiento del usuario automáticamente al valor solicitado.",
"oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)",
"oauth_storage_quota_default_description": "Cuota (en GiB) que se usará cuando no se solicite un valor específico.",
"oauth_timeout": "Límite de tiempo para la solicitud",
"oauth_timeout": "Tiempo de espera agotado para la solicitud",
"oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos",
"password_enable_description": "Iniciar sesión con correo electrónico y contraseña",
"password_settings": "Contraseña de Acceso",
@@ -1603,8 +1603,8 @@
"remove_from_locked_folder": "Eliminar de la carpeta protegida",
"remove_from_locked_folder_confirmation": "¿Seguro que deseas sacar estas fotos y vídeos de la carpeta protegida? Si continúas, los elementos serán visibles en tu biblioteca.",
"remove_from_shared_link": "Eliminar desde enlace compartido",
"remove_memory": "Quitar memoria",
"remove_photo_from_memory": "Quitar foto de esta memoria",
"remove_memory": "Quitar recuerdo",
"remove_photo_from_memory": "Quitar foto de este recuerdo",
"remove_tag": "Quitar etiqueta",
"remove_url": "Eliminar URL",
"remove_user": "Eliminar usuario",
@@ -1612,8 +1612,8 @@
"removed_from_archive": "Eliminado del archivo",
"removed_from_favorites": "Eliminado de favoritos",
"removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos",
"removed_memory": "Memoria eliminada",
"removed_photo_from_memory": "Se ha eliminado la foto de la memoria",
"removed_memory": "Recuerdo eliminado",
"removed_photo_from_memory": "Foto eliminada del recuerdo",
"removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}",
"rename": "Renombrar",
"repair": "Reparar",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Ava otsingufiltrid",
"options": "Valikud",
"or": "või",
"organize_into_albums": "Organiseeri albumitesse",
"organize_into_albums_description": "Pane olemasolevad fotod albumitesse, kasutades jooksvaid sünkroonimise seadeid",
"organize_your_library": "Korrasta oma kogu",
"original": "originaal",
"other": "Muud",
@@ -1735,7 +1737,7 @@
"select_user_for_sharing_page_err_album": "Albumi lisamine ebaõnnestus",
"selected": "Valitud",
"selected_count": "{count, plural, other {# valitud}}",
"selected_gps_coordinates": "valitud GPS-koordinaadid",
"selected_gps_coordinates": "Valitud GPS-koordinaadid",
"send_message": "Saada sõnum",
"send_welcome_email": "Saada tervituskiri",
"server_endpoint": "Serveri lõpp-punkt",
@@ -2075,6 +2077,7 @@
"view_next_asset": "Vaata järgmist üksust",
"view_previous_asset": "Vaata eelmist üksust",
"view_qr_code": "Vaata QR-koodi",
"view_similar_photos": "Vaata sarnaseid fotosid",
"view_stack": "Vaata virna",
"view_user": "Vaata kasutajat",
"viewer_remove_from_stack": "Eemalda virnast",

View File

@@ -396,6 +396,7 @@
"advanced_settings_prefer_remote_title": "Suosi etäkuvia",
"advanced_settings_proxy_headers_subtitle": "Määritä välityspalvelimen otsikot(proxy headers), jotka Immichin tulisi lähettää jokaisen verkkopyynnön mukana",
"advanced_settings_proxy_headers_title": "Välityspalvelimen otsikot",
"advanced_settings_readonly_mode_title": "Vain luku -tila",
"advanced_settings_self_signed_ssl_subtitle": "Ohita SSL sertifikaattivarmennus palvelimen päätepisteellä. Vaaditaan self-signed -sertifikaateissa.",
"advanced_settings_self_signed_ssl_title": "Salli self-signed SSL -sertifikaatit",
"advanced_settings_sync_remote_deletions_subtitle": "Poista tai palauta kohde automaattisesti tällä laitteella, kun kyseinen toiminto suoritetaan verkossa",
@@ -866,7 +867,7 @@
"edit_title": "Muokkaa otsikkoa",
"edit_user": "Muokkaa käyttäjää",
"edited": "Muokattu",
"editor": "Editori",
"editor": "Muokkaaja",
"editor_close_without_save_prompt": "Muutoksia ei tallenneta",
"editor_close_without_save_title": "Suljetaanko editori?",
"editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet",
@@ -1633,8 +1634,8 @@
"review_duplicates": "Tarkastele kaksoiskappaleita",
"review_large_files": "Tarkista suuret tiedostot",
"role": "Rooli",
"role_editor": "Editori",
"role_viewer": "Toistin",
"role_editor": "Muokkaaja",
"role_viewer": "Katsoja",
"running": "Käynnissä",
"save": "Tallenna",
"save_to_gallery": "Tallenna galleriaan",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Ouvrir les filtres de recherche",
"options": "Options",
"or": "ou",
"organize_into_albums": "Organiser dans des albums",
"organize_into_albums_description": "Mettre les photos existantes dans des albums en utilisant les paramètres de synchronisation actuels",
"organize_your_library": "Organiser votre bibliothèque",
"original": "original",
"other": "Autre",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Statut de contributeur",
"purchase_server_title": "Serveur",
"purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur",
"query_asset_id": "ID du média requis",
"queue_status": "{count}/{total} en file d'attente",
"rating": "Étoile d'évaluation",
"rating_clear": "Effacer l'évaluation",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
"selected": "Sélectionné",
"selected_count": "{count, plural, one {# sélectionné} other {# sélectionnés}}",
"selected_gps_coordinates": "coordonnées GPS sélectionnées",
"selected_gps_coordinates": "Coordonnées GPS sélectionnées",
"send_message": "Envoyer un message",
"send_welcome_email": "Envoyer un courriel de bienvenue",
"server_endpoint": "Adresse du serveur",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Voir le média suivant",
"view_previous_asset": "Voir le média précédent",
"view_qr_code": "Voir le QR code",
"view_similar_photos": "Voir les photos similaires",
"view_stack": "Afficher la pile",
"view_user": "Voir l'utilisateur",
"viewer_remove_from_stack": "Retirer de la pile",

View File

@@ -396,6 +396,8 @@
"advanced_settings_prefer_remote_title": "העדף תמונות מרוחקות",
"advanced_settings_proxy_headers_subtitle": "הגדר proxy headers שהיישום צריך לשלוח עם כל בקשת רשת",
"advanced_settings_proxy_headers_title": "כותרות פרוקסי",
"advanced_settings_readonly_mode_subtitle": "מאפשר את מצב לקריאה בלבד בו התמונות ניתנות לצפייה בלבד, דברים כמו בחירת תמונות מרובות, שיתוף, שידור, מחיקה הם כולם מושבתים. אפשר/השבת מצב לקריאה בלבד באמצעות יצגן המשתמש מהמסך הראשי",
"advanced_settings_readonly_mode_title": "מצב לקריאה בלבד",
"advanced_settings_self_signed_ssl_subtitle": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית.",
"advanced_settings_self_signed_ssl_title": "התר תעודות SSL בחתימה עצמית",
"advanced_settings_sync_remote_deletions_subtitle": "מחק או שחזר תמונה במכשיר זה באופן אוטומטי כאשר פעולה זו נעשית בדפדפן",
@@ -461,6 +463,7 @@
"app_bar_signout_dialog_title": "התנתק",
"app_settings": "הגדרות יישום",
"appears_in": "מופיע ב",
"apply_count": "החל ({count, number})",
"archive": "ארכיון",
"archive_action_prompt": "{count} נוספו לארכיון",
"archive_or_unarchive_photo": "העבר תמונה לארכיון או הוצא אותה משם",
@@ -842,7 +845,7 @@
"duration": "משך זמן",
"edit": "ערוך",
"edit_album": "ערוך אלבום",
"edit_avatar": "ערוך תמונת פרופיל",
"edit_avatar": "ערוך יצגן",
"edit_birthday": "עריכת יום הולדת",
"edit_date": "ערוך תאריך",
"edit_date_and_time": "ערוך תאריך ושעה",
@@ -1073,12 +1076,18 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "תכונה זאת טוענת משאבים חיצוניים מגוגל בכדי לפעול.",
"general": "כללי",
"geolocation_instruction_all_have_location": "לכל הפריטים עבור תאריך זה כבר יש נתוני מיקום. נסה להציג את כל הפריטים או בחר תאריך אחר",
"geolocation_instruction_location": "לחץ על פריט עם קואורדינטות GPS כדי להשתמש במיקומו, או בחר מיקום ישירות מהמפה",
"geolocation_instruction_no_date": "בחר תאריך כדי לנהל נתוני מיקום עבור תמונות וסרטונים מאותו יום",
"geolocation_instruction_no_photos": "לא נמצאו תמונות או סרטונים עבור תאריך זה. בחר תאריך אחר כדי להציג אותם",
"get_help": "קבל עזרה",
"get_wifiname_error": "לא היה ניתן לקבל את שם האינטרנט האלחוטי שלך. יש לודא שהענקת את ההרשאות הדרושות ושאת/ה מחובר/ת לרשת אינטרנט אלחוטי",
"getting_started": "תחילת העבודה",
"go_back": "חזור",
"go_to_folder": "עבור לתיקיה",
"go_to_search": "עבור לחיפוש",
"gps": "GPS",
"gps_missing": "אין GPS",
"grant_permission": "להעניק הרשאה",
"group_albums_by": "קבץ אלבומים לפי..",
"group_country": "קבץ לפי מדינה",
@@ -1262,6 +1271,7 @@
"main_branch_warning": "הגרסה המותקנת היא גרסת פיתוח; אנחנו ממליצים בחום להשתמש בגרסה יציבה!",
"main_menu": "תפריט ראשי",
"make": "תוצרת",
"manage_geolocation": "נהל מיקום",
"manage_shared_links": "ניהול קישורים משותפים",
"manage_sharing_with_partners": "ניהול שיתוף עם שותפים",
"manage_the_app_settings": "ניהול הגדרות האפליקציה",
@@ -1508,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "גרסת היישום לנייד מיושנת. נא לעדכן לגרסה המשנית האחרונה.",
"profile_drawer_client_server_up_to_date": "היישום והשרת מעודכנים",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "מצב לקריאה בלבד מופעל. הקש הקשה כפולה על סמל היצגן של המשתמש כדי לצאת.",
"profile_drawer_server_out_of_date_major": "השרת אינו מעודכן. נא לעדכן לגרסה הראשית האחרונה.",
"profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה המשנית האחרונה.",
"profile_image_of_user": "תמונת פרופיל של {user}",
@@ -1553,6 +1564,8 @@
"rating_description": "הצג את דירוג ה-EXIF בלוח המידע",
"reaction_options": "אפשרויות הגבה",
"read_changelog": "קרא את יומן השינויים",
"readonly_mode_disabled": "מצב לקריאה בלבד מושבת",
"readonly_mode_enabled": "מצב לקריאה בלבד מופעל",
"reassign": "הקצה מחדש",
"reassigned_assets_to_existing_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש אל {name, select, null {אדם קיים} other {{name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש לאדם חדש",
@@ -1709,7 +1722,7 @@
"select_all": "בחר הכל",
"select_all_duplicates": "בחר את כל הכפילויות",
"select_all_in": "בחר הכול בתוך {group}",
"select_avatar_color": "בחר צבע תמונת פרופיל",
"select_avatar_color": "בחר צבע יצגן",
"select_face": "בחר פנים",
"select_featured_photo": "בחר תמונה מייצגת",
"select_from_computer": "בחר מהמחשב",
@@ -1722,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "יצירת אלבום נכשלה",
"selected": "נבחרו",
"selected_count": "{count, plural, other {# נבחרו}}",
"selected_gps_coordinates": "קואורדינטות GPS שנבחרו",
"send_message": "שלח הודעה",
"send_welcome_email": "שלח דוא\"ל קבלת פנים",
"server_endpoint": "נקודת קצה שרת",
@@ -1832,8 +1846,10 @@
"shift_to_permanent_delete": "לחץ ⇧ כדי למחוק תמונה לצמיתות",
"show_album_options": "הצג אפשרויות אלבום",
"show_albums": "הצג אלבומים",
"show_all_assets": "הצג את כל הפריטים",
"show_all_people": "הצג את כל האנשים",
"show_and_hide_people": "הצג & הסתר אנשים",
"show_assets_without_location": "הצג פריטים ללא מיקום",
"show_file_location": "הצג את מיקום הקובץ",
"show_gallery": "הצג גלריה",
"show_hidden_people": "הצג אנשים מוסתרים",
@@ -1993,6 +2009,7 @@
"unstacked_assets_count": "{count, plural, one {תמונה # הוסרה} other {# תמונות הוסרו}} מהערימה",
"untagged": "לא מתיוגים",
"up_next": "הבא בתור",
"update_location_action_prompt": "עדכן את המיקום של {count} פריטים שנבחרו עם:",
"updated_at": "עודכן",
"updated_password": "סיסמה עודכנה",
"upload": "העלאה",
@@ -2017,6 +2034,7 @@
"use_biometric": "השתמש באימות ביומטרי",
"use_current_connection": "השתמש בחיבור נוכחי",
"use_custom_date_range": "השתמש בטווח תאריכים מותאם במקום",
"use_this_location": "לחץ כדי להשתמש במיקום",
"user": "משתמש",
"user_has_been_deleted": "משתמש זה נמחק.",
"user_id": "מזהה משתמש",

View File

@@ -28,6 +28,7 @@
"add_to_album": "Tambahkan ke album",
"add_to_album_bottom_sheet_added": "Ditambahkan ke {album}",
"add_to_album_bottom_sheet_already_exists": "Sudah ada di {album}",
"add_to_albums": "Tambahkan ke album",
"add_to_shared_album": "Tambahkan ke album terbagi",
"add_url": "Tambahkan URL",
"added_to_archive": "Ditambahkan ke arsip",

View File

@@ -392,10 +392,12 @@
"advanced_settings_enable_alternate_media_filter_subtitle": "Usa questa opzione per filtrare i contenuti multimediali durante la sincronizzazione in base a criteri alternativi. Prova questa opzione solo se riscontri problemi con il rilevamento di tutti gli album da parte dell'app.",
"advanced_settings_enable_alternate_media_filter_title": "[SPERIMENTALE] Usa un filtro alternativo per la sincronizzazione degli album del dispositivo",
"advanced_settings_log_level_title": "Livello log: {level}",
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini locali. Attivare questa impostazione per caricare invece le immagini remote.",
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono estremamente lenti a caricare le miniature da risorse locali. Attiva questa impostazione per caricare invece le immagini remote.",
"advanced_settings_prefer_remote_title": "Preferisci immagini remote",
"advanced_settings_proxy_headers_subtitle": "Definisci gli header per i proxy che Immich dovrebbe inviare con ogni richiesta di rete",
"advanced_settings_proxy_headers_title": "Header Proxy",
"advanced_settings_readonly_mode_subtitle": "Abilita la modalità di sola lettura in cui le foto possono essere solo visualizzate, mentre funzioni come la selezione di più immagini, la condivisione, la trasmissione e l'eliminazione sono tutte disabilitate. Abilita/Disabilita la sola lettura tramite l'avatar dell'utente dalla schermata principale",
"advanced_settings_readonly_mode_title": "Modalità di sola lettura",
"advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.",
"advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed",
"advanced_settings_sync_remote_deletions_subtitle": "Rimuovi o ripristina automaticamente un elemento su questo dispositivo quando l'azione è stata fatta via web",
@@ -461,6 +463,7 @@
"app_bar_signout_dialog_title": "Disconnetti",
"app_settings": "Impostazioni Applicazione",
"appears_in": "Compare in",
"apply_count": "Applica ({count, number})",
"archive": "Archivio",
"archive_action_prompt": "Aggiunti {count} elementi all'Archivio",
"archive_or_unarchive_photo": "Archivia o ripristina foto",
@@ -1073,12 +1076,18 @@
"gcast_enabled": "Google Cast Abilitato",
"gcast_enabled_description": "Questa funzione carica risorse esterne da Google per poter funzionare.",
"general": "Generale",
"geolocation_instruction_all_have_location": "Tutte le risorse per questa data hanno già dati sulla posizione. Prova a mostrare tutte le risorse o seleziona una data diversa",
"geolocation_instruction_location": "Fai clic su una risorsa con coordinate GPS per utilizzare la sua posizione oppure seleziona una posizione direttamente dalla mappa",
"geolocation_instruction_no_date": "Seleziona una data per gestire i dati sulla posizione per foto e video di quel giorno",
"geolocation_instruction_no_photos": "Nessuna foto o video trovato per questa data. Seleziona una data diversa per visualizzarli",
"get_help": "Chiedi Aiuto",
"get_wifiname_error": "Non sono riuscito a recuperare il nome della rete Wi-Fi. Accertati di aver concesso i permessi necessari e di essere connesso ad una rete Wi-Fi",
"getting_started": "Iniziamo",
"go_back": "Torna indietro",
"go_to_folder": "Vai alla cartella",
"go_to_search": "Vai alla ricerca",
"gps": "GPS",
"gps_missing": "No GPS",
"grant_permission": "Concedi permesso",
"group_albums_by": "Raggruppa album in base a...",
"group_country": "Raggruppa per paese",
@@ -1181,7 +1190,7 @@
"language": "Lingua",
"language_no_results_subtitle": "Prova a cambiare i tuoi termini di ricerca",
"language_no_results_title": "Linguaggi non trovati",
"language_search_hint": "Cerca linguaggi...",
"language_search_hint": "Cerca una lingua...",
"language_setting_description": "Seleziona la tua lingua predefinita",
"large_files": "File pesanti",
"last": "Ultimo",
@@ -1262,6 +1271,7 @@
"main_branch_warning": "Stai utilizzando una versione di sviluppo. Ti consigliamo vivamente di utilizzare una versione di rilascio!",
"main_menu": "Menu Principale",
"make": "Produttore",
"manage_geolocation": "Gestisci posizione",
"manage_shared_links": "Gestisci link condivisi",
"manage_sharing_with_partners": "Gestisci la condivisione con i compagni",
"manage_the_app_settings": "Gestisci le impostazioni dell'applicazione",
@@ -1508,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Aggiorna all'ultima versione minore.",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Modalità di sola lettura abilitata. Tocca due volte l'icona dell'avatar dell'utente per disabilitarla.",
"profile_drawer_server_out_of_date_major": "Il server non è aggiornato. Aggiorna all'ultima versione principale.",
"profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Aggiorna all'ultima versione minore.",
"profile_image_of_user": "Immagine profilo di {user}",
@@ -1553,6 +1564,8 @@
"rating_description": "Visualizza la valutazione EXIF nel pannello informazioni",
"reaction_options": "Impostazioni Reazioni",
"read_changelog": "Leggi Riepilogo Modifiche",
"readonly_mode_disabled": "Modalità di sola lettura disabilitata",
"readonly_mode_enabled": "Modalità di sola lettura abilitata",
"reassign": "Riassegna",
"reassigned_assets_to_existing_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} {name, select, null {ad una persona esistente} other {a {name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} ad una nuova persona",
@@ -1722,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "Impossibile nel creare l'album",
"selected": "Selezionato",
"selected_count": "{count, plural, one {# selezionato} other {# selezionati}}",
"selected_gps_coordinates": "coordinate GPS selezionate",
"send_message": "Manda messaggio",
"send_welcome_email": "Invia email di benvenuto",
"server_endpoint": "Server endpoint",
@@ -1832,8 +1846,10 @@
"shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente l'asset",
"show_album_options": "Mostra opzioni album",
"show_albums": "Mostra gli album",
"show_all_assets": "Mostra tutte le risorse",
"show_all_people": "Mostra tutte le persone",
"show_and_hide_people": "Mostra & nascondi persone",
"show_assets_without_location": "Mostra risorse senza posizione",
"show_file_location": "Mostra percorso file",
"show_gallery": "Mostra galleria",
"show_hidden_people": "Mostra persone nascoste",
@@ -1993,6 +2009,7 @@
"unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # asset}}",
"untagged": "Senza tag",
"up_next": "Prossimo",
"update_location_action_prompt": "Aggiorna la posizione di {count} risorse selezionate con:",
"updated_at": "Aggiornato il",
"updated_password": "Password aggiornata",
"upload": "Carica",
@@ -2017,6 +2034,7 @@
"use_biometric": "Usa biometrica",
"use_current_connection": "usa la connessione attuale",
"use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato",
"use_this_location": "Clicca per usare la posizione",
"user": "Utente",
"user_has_been_deleted": "L'utente è stato rimosso.",
"user_id": "ID utente",

View File

@@ -392,7 +392,7 @@
"advanced_settings_enable_alternate_media_filter_subtitle": "別の基準に従ってメディアファイルにフィルターをかけて、同期を行います。アプリがすべてのアルバムを読み込んでくれない場合にのみ、この機能を試してください。",
"advanced_settings_enable_alternate_media_filter_title": "[試験運用] 別のデバイスのアルバム同期フィルターを使用する",
"advanced_settings_log_level_title": "ログレベル: {level}",
"advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションを有効にする事により、サーバーから直接画像をロードすることが可能です。",
"advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションを有効にする事により、サーバーから直接画像をロードすることが可能です。",
"advanced_settings_prefer_remote_title": "リモートを優先する",
"advanced_settings_proxy_headers_subtitle": "プロキシヘッダを設定する",
"advanced_settings_proxy_headers_title": "プロキシヘッダ",

View File

@@ -1081,6 +1081,8 @@
"go_back": "뒤로",
"go_to_folder": "폴더로 이동",
"go_to_search": "검색으로 이동",
"gps": "GPS",
"gps_missing": "GPS 없음",
"grant_permission": "권한 부여",
"group_albums_by": "다음으로 앨범 그룹화...",
"group_country": "국가별로 그룹화",
@@ -1264,6 +1266,7 @@
"main_branch_warning": "개발 버전을 사용 중입니다. 정식 릴리스 버전 사용을 권장합니다!",
"main_menu": "메인 메뉴",
"make": "제조사",
"manage_geolocation": "위치 정보 관리",
"manage_shared_links": "공유 링크 관리",
"manage_sharing_with_partners": "공유할 파트너를 초대하거나 제거합니다.",
"manage_the_app_settings": "앱 동작 및 표시 환경을 사용자 정의합니다.",

View File

@@ -156,13 +156,18 @@
"password_enable_description": "Pieteikšanās ar e-pasta adresi un paroli",
"password_settings": "Pieteikšanās ar paroli",
"password_settings_description": "Pieteikšanās ar paroli iestatījumu pārvaldība",
"paths_validated_successfully": "Visi ceļi veiksmīgi pārbaudīti",
"person_cleanup_job": "Personu tīrīšana",
"quota_size_gib": "Kvotas izmērs (GiB)",
"refreshing_all_libraries": "Atsvaidzina visas bibliotēkas",
"registration": "Administratora reģistrācija",
"registration_description": "Tā kā tu esi pirmais sistēmas lietotājs, tev tiks piešķirts administratora statuss un tu būsi atbildīgs par administrēšanas uzdevumiem, kā arī par citu lietotāju izveidi.",
"require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās",
"reset_settings_to_default": "Atjaunot iestatījumus uz noklusējuma vērtībām",
"reset_settings_to_recent_saved": "Atjaunot iestatījumus uz pēdējiem saglabātajiem iestatījumiem",
"scanning_library": "Skenē bibliotēku",
"search_jobs": "Meklēt uzdevumus…",
"send_welcome_email": "Nosūtīt sveiciena e-pastu",
"server_external_domain_settings": "Ārējais domēns",
"server_external_domain_settings_description": "Domēns publiski kopīgotajām saitēm, iekļaujot http(s)://",
"server_public_users": "Publiski lietotāji",
@@ -187,6 +192,7 @@
"theme_custom_css_settings_description": "Cascading Style Sheets ļauj pielāgot Immich izskatu.",
"theme_settings_description": "Immich tīmekļa saskarnes pielāgojumu pārvaldība",
"thumbnail_generation_job": "Sīktēlu ģenerēšana",
"thumbnail_generation_job_description": "Izveidot lielu, mazu un izplūdušu sīktēlu katram failam, kā arī sīktēlu katrai personai",
"transcoding_acceleration_api": "Paātrināšanas API",
"transcoding_acceleration_nvenc": "NVENC (nepieciešams NVIDIA GPU)",
"transcoding_acceleration_qsv": "Quick Sync (nepieciešams 7. paaudzes vai jaunāks Intel procesors)",
@@ -205,10 +211,13 @@
"trash_number_of_days": "Dienu skaits",
"trash_settings": "Atkritnes iestatījumi",
"trash_settings_description": "Atkritnes iestatījumu pārvaldība",
"user_delete_delay_settings": "Dzēšanas aizture",
"user_delete_delay_settings_description": "Dienu skaits pēc izdzēšanas, kad neatgriezeniski tiks dzēsti lietotāja konti un faili. Lietotāju dzēšanas uzdevums tiek izpildīts pusnaktī un pārbauda, kuri lietotāji ir gatavi dzēšanai. Izmaiņas šajā iestatījumā tiks ņemtas vērā nākamajā izpildes reizē.",
"user_delete_immediately_checkbox": "Ierindot lietotāju un failus tūlītējai dzēšanai",
"user_details": "Lietotāja informācija",
"user_management": "Lietotāju pārvaldība",
"user_password_has_been_reset": "Lietotāja parole ir atiestatīta:",
"user_password_reset_description": "Lūdzu, norādi lietotājam pagaidu paroli un informē viņu, ka nākamajā pieslēgšanās reizē viņam būs jāmaina parole.",
"user_restore_description": "<b>{user}</b> konts tiks atjaunots.",
"user_restore_scheduled_removal": "Atjaunot lietotāju - plānotā dzēšana {date, date, long}",
"user_settings": "Lietotāja iestatījumi",
@@ -238,6 +247,7 @@
"album_added": "Albums pievienots",
"album_added_notification_setting_description": "Saņemt e-pasta paziņojumu, kad tevi pievieno kopīgam albumam",
"album_cover_updated": "Albuma attēls atjaunināts",
"album_delete_confirmation_description": "Ja šis albums tiek kopīgots, citi lietotāji vairs nevarēs tam piekļūt.",
"album_deleted": "Albums dzēsts",
"album_info_card_backup_album_excluded": "NEIEKĻAUTS",
"album_info_card_backup_album_included": "IEKĻAUTS",
@@ -279,6 +289,7 @@
"app_bar_signout_dialog_title": "Izrakstīties",
"app_settings": "Lietotnes iestatījumi",
"appears_in": "Parādās iekš",
"apply_count": "Pielietot ({count, number})",
"archive": "Arhīvs",
"archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs",
"archive_page_title": "Arhīvs ({count})",
@@ -337,6 +348,7 @@
"backup_controller_page_background_app_refresh_enable_button_text": "Doties uz iestatījumiem",
"backup_controller_page_background_battery_info_link": "Parādīt, kā",
"backup_controller_page_background_battery_info_message": "Lai iegūtu vislabāko fona dublēšanas pieredzi, lūdzu, atspējojiet visas akumulatora optimizācijas, kas ierobežo Immich fona aktivitāti.\n\nTā kā katrai ierīcei iestatījumi ir citādāki, lūdzu, meklējiet nepieciešamo informāciju pie ierīces ražotāja.",
"backup_controller_page_background_battery_info_ok": "Labi",
"backup_controller_page_background_battery_info_title": "Akumulatora optimizācija",
"backup_controller_page_background_charging": "Tikai uzlādes laikā",
"backup_controller_page_background_configure_error": "Neizdevās konfigurēt fona pakalpojumu",
@@ -406,6 +418,7 @@
"cache_settings_title": "Kešdarbes iestatījumi",
"camera": "Fotokamera",
"cancel": "Atcelt",
"canceled": "Atcelts",
"canceling": "Atceļ",
"cannot_merge_people": "Nevar apvienot personas",
"cast": "Pārraidīt",
@@ -424,12 +437,19 @@
"change_password_form_password_mismatch": "Paroles nesakrīt",
"change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli",
"change_pin_code": "Nomainīt PIN kodu",
"check_corrupt_asset_backup_button": "Veikt pārbaudi",
"choose_matching_people_to_merge": "Izvēlies atbilstošas personas apvienošanai",
"city": "Pilsēta",
"clear": "Notīrīt",
"clear_all": "Notīrīt visu",
"clear_file_cache": "Notīrīt failu kešatmiņu",
"clear_value": "Notīrīt vērtību",
"client_cert_dialog_msg_confirm": "Labi",
"client_cert_enter_password": "Ievadi paroli",
"client_cert_import": "Importēt",
"client_cert_import_success_msg": "Klienta sertifikāts ir importēts",
"client_cert_invalid_msg": "Nederīgs sertifikāta fails vai nepareiza parole",
"client_cert_remove_msg": "Klienta sertifikāts ir noņemts",
"client_cert_subtitle": "Atbalsta tikai PKCS12 (.p12, .pfx) formātu. Sertifikātu importēšana/noņemšana ir pieejama tikai pirms pieslēgšanās",
"client_cert_title": "SSL klienta sertifikāts",
"clockwise": "Pulksteņrādītāja virzienā",
@@ -441,6 +461,7 @@
"comment_deleted": "Komentārs dzēsts",
"common_create_new_album": "Izveidot jaunu albumu",
"common_server_error": "Lūdzu, pārbaudiet tīkla savienojumu, pārliecinieties, vai serveris ir sasniedzams un aplikācijas/servera versijas ir saderīgas.",
"completed": "Pabeigts",
"confirm": "Apstiprināt",
"confirm_new_pin_code": "Apstiprināt jauno PIN kodu",
"confirm_password": "Apstiprināt paroli",
@@ -463,6 +484,7 @@
"create_library": "Izveidot bibliotēku",
"create_link": "Izveidot saiti",
"create_link_to_share": "Izveidot kopīgošanas saiti",
"create_new": "IZVEIDOT JAUNU",
"create_new_person": "Izveidot jaunu personu",
"create_new_user": "Izveidot jaunu lietotāju",
"create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS",
@@ -471,6 +493,7 @@
"created_at": "Izveidots",
"curated_object_page_title": "Lietas",
"current_pin_code": "Esošais PIN kods",
"current_server_address": "Pašreizējā servera adrese",
"custom_locale": "Pielāgota lokalizācija",
"custom_locale_description": "Formatēt datumus un skaitļus atbilstoši valodai un reģionam",
"custom_url": "Pielāgots URL",
@@ -524,21 +547,27 @@
"download_action_prompt": "Lejupielādē {count} failus",
"download_canceled": "Lejupielāde atcelta",
"download_complete": "Lejupielāde pabeigta",
"download_enqueue": "Lejupielāde ierindota",
"download_error": "Lejupielādes kļūda",
"download_failed": "Lejupielāde neizdevās",
"download_finished": "Lejupielāde pabeigta",
"download_notfound": "Lejupielāde nav atrasta",
"download_paused": "Lejupielāde nopauzēta",
"download_settings": "Lejupielāde",
"download_settings_description": "Ar failu lejupielādi saistīto iestatījumu pārvaldība",
"download_started": "Lejupielāde sākta",
"download_sucess": "Lejupielāde izdevās",
"download_sucess_android": "Multivides fails ir lejupielādēts uz DCIM/Immich",
"download_waiting_to_retry": "Gaida, lai mēģinātu atkārtoti",
"downloading": "Lejupielādē",
"downloading_asset_filename": "Lejupielādē failu {filename}",
"downloading_media": "Lejupielādē failu",
"duplicates": "Dublikāti",
"duplicates_description": "Atrisini katru grupu, norādot, kuri no tiem ir dublikāti",
"duration": "Ilgums",
"edit": "Labot",
"edit_album": "Labot albumu",
"edit_avatar": "Labot avatāru",
"edit_birthday": "Labot dzimšanas dienu",
"edit_date": "Labot datumu",
"edit_date_and_time": "Labot datumu un laiku",
@@ -572,6 +601,7 @@
"enter_your_pin_code": "Ievadi savu PIN kodu",
"enter_your_pin_code_subtitle": "Ievadi savu PIN kodu, lai piekļūtu slēgtajai mapei",
"error": "Kļūda",
"error_change_sort_album": "Neizdevās nomainīt albuma kārtošanas secību",
"error_saving_image": "Kļūda: {error}",
"errors": {
"cant_get_faces": "Nevar iegūt sejas",
@@ -625,10 +655,13 @@
"export_database_description": "Eksportēt SQLite datubāzi",
"extension": "Paplašinājums",
"external": "Ārējs",
"external_network": "Ārējs tīkls",
"external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
"face_unassigned": "Nepiešķirts",
"failed": "Neizdevās",
"failed_to_authenticate": "Neizdevās autentificēties",
"failed_to_load_assets": "Neizdevās ielādēt failus",
"failed_to_load_folder": "Neizdevās ielādēt mapi",
"favorite": "Izlase",
"favorites": "Izlase",
"favorites_page_no_favorites": "Nav atrasti iecienītākie faili",
@@ -654,6 +687,8 @@
"go_back": "Doties atpakaļ",
"go_to_folder": "Doties uz mapi",
"go_to_search": "Doties uz meklēšanu",
"gps": "Ir koordinātas",
"gps_missing": "Nav koordinātu",
"grant_permission": "Piešķirt atļauju",
"group_albums_by": "Grupēt albumus pēc...",
"group_country": "Grupēt pēc valsts",
@@ -789,6 +824,7 @@
"look": "Izskats",
"loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaļu skatītājā.",
"make": "Ražotājs",
"manage_geolocation": "Pārvaldīt atrašanās vietu",
"manage_shared_links": "Kopīgoto saišu pārvaldība",
"manage_sharing_with_partners": "Koplietošanas ar partneriem pārvaldība",
"manage_the_app_settings": "Lietotnes iestatījumu pārvaldība",
@@ -856,6 +892,7 @@
"name_or_nickname": "Vārds vai iesauka",
"network_requirement_photos_upload": "Izmantot mobilo datu pārraidi, lai dublētu fotoattēlus",
"network_requirement_videos_upload": "Izmantot mobilo datu pārraidi, lai dublētu video",
"networking_subtitle": "Pārvaldīt servera galapunktu iestatījumus",
"never": "nekad",
"new_album": "Jauns albums",
"new_api_key": "Jauna API atslēga",
@@ -881,6 +918,7 @@
"no_results": "Nav rezultātu",
"no_results_description": "Izmēģiniet sinonīmu vai vispārīgāku atslēgvārdu",
"not_in_any_album": "Nav nevienā albumā",
"not_selected": "Nav izvēlēts",
"notes": "Piezīmes",
"nothing_here_yet": "Šeit vēl nekā nav",
"notification_permission_dialog_content": "Lai iespējotu paziņojumus, atveriet Iestatījumi un atlasiet Atļaut.",
@@ -1115,12 +1153,17 @@
"see_all_people": "Skatīt visas personas",
"select_album_cover": "Izvēlieties albuma vāciņu",
"select_all_duplicates": "Atlasīt visus dublikātus",
"select_avatar_color": "Izvēlies avatāra krāsu",
"select_face": "Izvēlies seju",
"select_from_computer": "Izvēlēties no datora",
"select_keep_all": "Atzīmēt visus paturēšanai",
"select_library_owner": "Izvēlies bibliotēkas īpašnieku",
"select_new_face": "Izvēlies jaunu seju",
"select_photos": "Fotoattēlu Izvēle",
"select_trash_all": "Atzīmēt visus dzēšanai",
"select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu",
"selected": "Izvēlētie",
"selected_gps_coordinates": "izvēlētās ģeogrāfiskās koordinātas",
"server_info_box_app_version": "Aplikācijas Versija",
"server_info_box_server_url": "Servera URL",
"server_online": "Serveris tiešsaistē",
@@ -1202,6 +1245,7 @@
"sharing_silver_appbar_share_partner": "Dalīties ar partneri",
"show_album_options": "Rādīt albuma iespējas",
"show_albums": "Rādīt albumus",
"show_all_assets": "Rādīt visus failus",
"show_all_people": "Rādīt visas personas",
"show_and_hide_people": "Rādīt un slēpt personas",
"show_file_location": "Rādīt faila atrašanās vietu",
@@ -1325,6 +1369,7 @@
"usage": "Lietojums",
"use_biometric": "Izmantot biometrisko autentifikāciju",
"use_current_connection": "izmantot pašreizējo savienojumu",
"use_custom_date_range": "Izmantot pielāgotu datuma intervālu",
"user": "Lietotājs",
"user_has_been_deleted": "Šis lietotājs ir dzēsts.",
"user_id": "Lietotāja ID",
@@ -1338,6 +1383,7 @@
"username": "Lietotājvārds",
"users": "Lietotāji",
"utilities": "Rīki",
"validate": "Pārbaudīt",
"variables": "Mainīgie",
"version": "Versija",
"version_announcement_closing": "Tavs draugs, Alekss",

View File

@@ -13,6 +13,7 @@
"add_a_location": "ഒരു സ്ഥലം ചേർക്കുക",
"add_a_name": "ഒരു പേര് ചേർക്കുക",
"add_a_title": "ഒരു ശീർഷകം ചേർക്കുക",
"add_birthday": "ജന്മദിനം ചേർക്കുക",
"add_endpoint": "എൻഡ്പോയിന്റ് ചേർക്കുക",
"add_exclusion_pattern": "ഒഴിവാക്കാനുള്ള മാതൃക ചേർക്കുക",
"add_import_path": "ഇറക്കുമതി ചെയ്യുക",
@@ -22,10 +23,12 @@
"add_path": "പാത ചേര്‍ക്കുക",
"add_photos": "ചിത്രങ്ങള്‍ ചേര്‍ക്കുക",
"add_tag": "ടാഗ് ചേര്‍ക്കുക",
"add_to": "ചേര്‍ക്കുക",
"add_to": "ചേര്‍ക്കുക",
"add_to_album": "ആല്‍ബത്തിലേക്ക് ചേര്‍ക്കുക",
"add_to_album_bottom_sheet_added": "{album} - ലേക്ക് ചേര്‍ത്തു",
"add_to_album_bottom_sheet_already_exists": "{album} ആൽബത്തിൽ ഇപ്പോള്‍ തന്നെ ഉണ്ട്",
"add_to_albums": "ആൽബങ്ങളിൽ ചേർക്കുക",
"add_to_albums_count": "ആൽബങ്ങളിൽ ചേർക്കുക ({count})",
"add_to_shared_album": "പങ്കിട്ട ആൽബത്തിലേക്ക് ചേർക്കുക",
"add_url": "URL ചേര്‍ക്കുക",
"added_to_archive": "ചരിത്രരേഖയായി (ആര്‍ക്കൈവ്) ചേര്‍ത്തിരിക്കുന്നു",
@@ -68,6 +71,17 @@
"image_format": "ഘടന",
"image_format_description": "WebP ഉണ്ടാക്കാന്‍ സമയം എടുക്കും എങ്കിലും JPEG ഫയലുകളെക്കാള്‍ ചെറുതായിരിക്കും.",
"image_fullsize_description": "അധികവിവരങ്ങള്‍ ഒഴിവാക്കിയ ചിത്രം, വലുതാക്കി കാണിക്കുമ്പോള്‍ ഉപയോഗിക്കപ്പെടുന്നു",
"image_fullsize_enabled": "പൂര്‍ണ വലുപ്പത്തില്‍ ഉള്ള ചിത്രങ്ങള്‍ ഉണ്ടാക്കാന്‍പ്രാപ്തമാക്കുക"
}
"image_fullsize_enabled": "പൂര്‍ണ വലുപ്പത്തില്‍ ഉള്ള ചിത്രങ്ങള്‍ ഉണ്ടാക്കാന്‍പ്രാപ്തമാക്കുക",
"image_fullsize_quality_description": "1 മുതൽ 100 വരെയുള്ള പൂർണ്ണ വലുപ്പത്തിലുള്ള ഇമേജ് നിലവാരം. ഉയർന്നതാണ് നല്ലത്, പക്ഷേ വലിയ ഫയലുകൾ ഉത്പാദിപ്പിക്കുന്നു.",
"image_fullsize_title": "പൂർണ്ണ വലുപ്പത്തിലുള്ള ഇമേജ് ക്രമീകരണങ്ങൾ",
"image_quality": "ഗുണനിലവാരം",
"job_created": "ജോലി സൃഷ്ടിച്ചു",
"job_status": "ജോലി നില"
},
"waiting": "കാത്തിരിക്കുന്നു",
"warning": "മുന്നറിയിപ്പ്",
"week": "ആഴ്ച",
"welcome": "സ്വാഗതം",
"year": "വർഷം",
"yes": "അതെ"
}

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Åpne søkefiltrene",
"options": "Valg",
"or": "eller",
"organize_into_albums": "Organiser til albumer",
"organize_into_albums_description": "Plasser eksisterende bilder i albumer ved å bruke synkroniseringsinnstillinger",
"organize_your_library": "Organiser biblioteket ditt",
"original": "original",
"other": "Annet",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Støttespiller status",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren",
"query_asset_id": "Forespør objektID",
"queue_status": "Kø {count}/{total}",
"rating": "Stjernevurdering",
"rating_clear": "Slett vurdering",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Feilet ved oppretting av album",
"selected": "Valgt",
"selected_count": "{count, plural, other {# valgt}}",
"selected_gps_coordinates": "valgte GPS-koordinater",
"selected_gps_coordinates": "Valgte GPS-koordinater",
"send_message": "Send melding",
"send_welcome_email": "Send velkomstmelding",
"server_endpoint": "Server endepunkt",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Vis neste fil",
"view_previous_asset": "Vis forrige fil",
"view_qr_code": "Vis QR-kode",
"view_similar_photos": "Vis lignende bilder",
"view_stack": "Vis stabel",
"view_user": "Vis bruker",
"viewer_remove_from_stack": "Fjern fra stabling",

View File

@@ -396,6 +396,8 @@
"advanced_settings_prefer_remote_title": "Externe afbeeldingen laden",
"advanced_settings_proxy_headers_subtitle": "Definieer proxy headers die Immich bij elk netwerkverzoek moet verzenden",
"advanced_settings_proxy_headers_title": "Proxy headers",
"advanced_settings_readonly_mode_subtitle": "Schakelt de alleen-lezenmodus in, waarbij de foto's alleen bekeken kunnen worden. Dingen zoals het selecteren van meerdere afbeeldingen, delen, casten en verwijderen zijn allemaal uitgeschakeld. Schakel alleen-lezen in of uit via de gebruikers avatar vanaf het hoofdscherm",
"advanced_settings_readonly_mode_title": "Alleen-lezen Mode",
"advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten.",
"advanced_settings_self_signed_ssl_title": "Zelfondertekende SSL-certificaten toestaan",
"advanced_settings_sync_remote_deletions_subtitle": "Automatisch bestanden verwijderen of herstellen op dit apparaat als die actie op het web is ondernomen",
@@ -461,6 +463,7 @@
"app_bar_signout_dialog_title": "Log uit",
"app_settings": "App instellingen",
"appears_in": "Komt voor in",
"apply_count": "Toepassen ({count, number})",
"archive": "Archief",
"archive_action_prompt": "{count} item(s) toegevoegd aan het archief",
"archive_or_unarchive_photo": "Foto archiveren of uit het archief halen",
@@ -1073,12 +1076,18 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Deze functie gebruikt externe bronnen van Google om te kunnen werken.",
"general": "Algemeen",
"geolocation_instruction_all_have_location": "Alle items voor deze datum hebben al locatiegegevens. Probeer alle items te tonen of selecteer een andere datum",
"geolocation_instruction_location": "Klik op een item met GPS coördinaten om de locatie te gebruiken, of selecteer een locatie direct vanaf de kaart",
"geolocation_instruction_no_date": "Selecteer een datum om locatiegegevens te beheren voor foto's en video's van die dag",
"geolocation_instruction_no_photos": "Geen foto's of video's gevonden voor deze datum. Selecteer een andere datum om ze te tonen",
"get_help": "Krijg hulp",
"get_wifiname_error": "Kon de WiFi-naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een WiFi-netwerk",
"getting_started": "Aan de slag",
"go_back": "Ga terug",
"go_to_folder": "Ga naar map",
"go_to_search": "Ga naar zoeken",
"gps": "GPS",
"gps_missing": "Geen GPS",
"grant_permission": "Geef toestemming",
"group_albums_by": "Groepeer albums op...",
"group_country": "Groepeer op land",
@@ -1262,6 +1271,7 @@
"main_branch_warning": "Je gebruikt een ontwikkelingsversie. We raden je ten zeerste aan een releaseversie te gebruiken!",
"main_menu": "Hoofdmenu",
"make": "Merk",
"manage_geolocation": "Beheer locatie",
"manage_shared_links": "Beheer gedeelde links",
"manage_sharing_with_partners": "Beheer delen met partners",
"manage_the_app_settings": "Beheer de appinstellingen",
@@ -1508,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "Mobiele app is verouderd. Werk bij naar de nieuwste subversie.",
"profile_drawer_client_server_up_to_date": "App en server zijn up-to-date",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Alleen-lezen-modus ingeschakeld. Dubbeltik op het avatarpictogram van de gebruiker om te verlaten.",
"profile_drawer_server_out_of_date_major": "Server is verouderd. Werk bij naar de nieuwste hoofdversie.",
"profile_drawer_server_out_of_date_minor": "Server is verouderd. Werk bij naar de nieuwste subversie.",
"profile_image_of_user": "Profielfoto van {user}",
@@ -1553,6 +1564,8 @@
"rating_description": "De EXIF-waardering weergeven in het infopaneel",
"reaction_options": "Reactie-opties",
"read_changelog": "Lees wijzigingen",
"readonly_mode_disabled": "Alleen-lezen modus uitgeschakeld",
"readonly_mode_enabled": "Alleen-lezen modus ingeschakeld",
"reassign": "Opnieuw toewijzen",
"reassigned_assets_to_existing_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan {name, select, null {een bestaand persoon} other {{name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan een nieuw persoon",
@@ -1722,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "Album aanmaken mislukt",
"selected": "Geselecteerd",
"selected_count": "{count, plural, other {# geselecteerd}}",
"selected_gps_coordinates": "geselecteerde GPS coördinaten",
"send_message": "Bericht versturen",
"send_welcome_email": "Stuur welkomstmail",
"server_endpoint": "Server-URL",
@@ -1832,8 +1846,10 @@
"shift_to_permanent_delete": "druk op ⇧ om items permanent te verwijderen",
"show_album_options": "Toon albumopties",
"show_albums": "Toon albums",
"show_all_assets": "Toon alle items",
"show_all_people": "Toon alle mensen",
"show_and_hide_people": "Toon & verberg mensen",
"show_assets_without_location": "Toon items zonder locatie",
"show_file_location": "Toon bestandslocatie",
"show_gallery": "Toon galerij",
"show_hidden_people": "Verbogen mensen weergeven",
@@ -1941,7 +1957,9 @@
"to_change_password": "Wijzig wachtwoord",
"to_favorite": "Toevoegen aan favorieten",
"to_login": "Inloggen",
"to_multi_select": "naar multi-select",
"to_parent": "Ga naar hoofdmap",
"to_select": "naar selecteren",
"to_trash": "Prullenbak",
"toggle_settings": "Zichtbaarheid instellingen wisselen",
"total": "Totaal",
@@ -1991,6 +2009,7 @@
"unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld",
"untagged": "Ongemarkeerd",
"up_next": "Volgende",
"update_location_action_prompt": "Werk de locatie bij van {count} geselecteerde items met:",
"updated_at": "Geüpdatet",
"updated_password": "Wachtwoord bijgewerkt",
"upload": "Uploaden",
@@ -2015,6 +2034,7 @@
"use_biometric": "Gebruik biometrische authenticatie",
"use_current_connection": "gebruik huidige verbinding",
"use_custom_date_range": "Gebruik in plaats daarvan een aangepast datumbereik",
"use_this_location": "Klik om locatie te gebruiken",
"user": "Gebruiker",
"user_has_been_deleted": "Deze gebruiker is verwijderd.",
"user_id": "Gebruikers ID",

View File

@@ -44,7 +44,7 @@
"authentication_settings_description": "Zarządzaj hasłem, OAuth i innymi ustawienia uwierzytelnienia",
"authentication_settings_disable_all": "Czy jesteś pewny, że chcesz wyłączyć wszystkie metody logowania? Logowanie będzie całkowicie wyłączone.",
"authentication_settings_reenable": "Aby ponownie włączyć, użyj <link>Polecenia serwera</link>.",
"background_task_job": "Zadania w Tle",
"background_task_job": "Zadania w tle",
"backup_database": "Utwórz Zrzut Bazy Danych",
"backup_database_enable_description": "Włącz zrzuty bazy danych",
"backup_keep_last_amount": "Ile poprzednich zrzutów przechowywać",
@@ -396,6 +396,8 @@
"advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne",
"advanced_settings_proxy_headers_subtitle": "Zdefiniuj nagłówki proxy, które Immich powinien wysyłać z każdym żądaniem sieciowym",
"advanced_settings_proxy_headers_title": "Nagłówki proxy",
"advanced_settings_readonly_mode_subtitle": "Włącza tryb tylko do odczytu, w którym zdjęcia można tylko przeglądać, a takie czynności jak wybieranie wielu obrazów, udostępnianie, przesyłanie i usuwanie są wyłączone. Włącz/wyłącz tryb tylko do odczytu za pomocą awatara użytkownika na ekranie głównym",
"advanced_settings_readonly_mode_title": "Tryb tylko do odczytu",
"advanced_settings_self_signed_ssl_subtitle": "Pomija weryfikację certyfikatu SSL dla punktu końcowego serwera. Wymagane w przypadku certyfikatów z podpisem własnym.",
"advanced_settings_self_signed_ssl_title": "Zezwalaj na certyfikaty SSL z podpisem własnym",
"advanced_settings_sync_remote_deletions_subtitle": "Automatycznie usuń lub przywróć zasób na tym urządzeniu po wykonaniu tej czynności w interfejsie webowym",
@@ -459,8 +461,9 @@
"app_bar_signout_dialog_content": "Czy na pewno chcesz się wylogować?",
"app_bar_signout_dialog_ok": "Tak",
"app_bar_signout_dialog_title": "Wyloguj się",
"app_settings": "Ustawienia Aplikacji",
"app_settings": "Ustawienia aplikacji",
"appears_in": "W albumach",
"apply_count": "Zastosuj ({count, number})",
"archive": "Archiwum",
"archive_action_prompt": "{count} dodanych do Archiwum",
"archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum",
@@ -520,7 +523,7 @@
"assets_trashed_from_server": "{count} szt. usuniętych z serwera Immich",
"assets_were_part_of_album_count": "{count, plural, one {Zasób był} few {Zasoby były} many {Zasobów było} other {Zasobów było}} już częścią albumu",
"assets_were_part_of_albums_count": "{count, plural, one {Zasób był} other {Zasoby były}} już częścią albumów",
"authorized_devices": "Upoważnione Urządzenia",
"authorized_devices": "Autoryzowane urządzenia",
"automatic_endpoint_switching_subtitle": "Połącz się lokalnie przez wyznaczoną sieć Wi-Fi, jeśli jest dostępna, i korzystaj z alternatywnych połączeń gdzie indziej",
"automatic_endpoint_switching_title": "Automatyczne przełączanie adresów URL",
"autoplay_slideshow": "Automatyczne odtwarzanie pokazu slajdów",
@@ -528,7 +531,7 @@
"back_close_deselect": "Wróć, zamknij lub odznacz",
"background_location_permission": "Uprawnienia do lokalizacji w tle",
"background_location_permission_content": "Aby móc przełączać sieć podczas pracy w tle, Immich musi *zawsze* mieć dostęp do dokładnej lokalizacji, aby aplikacja mogła odczytać nazwę sieci Wi-Fi",
"backup": "Kopia Zapasowa",
"backup": "Kopia zapasowa",
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({count})",
"backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
"backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
@@ -538,12 +541,12 @@
"backup_all": "Wszystkie",
"backup_background_service_backup_failed_message": "Nie udało się wykonać kopii zapasowej zasobów. Ponowna próba…",
"backup_background_service_connection_failed_message": "Nie udało się połączyć z serwerem. Ponowna próba…",
"backup_background_service_current_upload_notification": "Wysyłanie {filename}",
"backup_background_service_current_upload_notification": "Przesyłanie {filename}",
"backup_background_service_default_notification": "Sprawdzanie nowych zasobów…",
"backup_background_service_error_title": "Błąd kopii zapasowej",
"backup_background_service_in_progress_notification": "Tworzenie kopii zapasowej twoich zasobów…",
"backup_background_service_upload_failure_notification": "Błąd przesyłania {filename}",
"backup_controller_page_albums": "Kopia Zapasowa albumów",
"backup_controller_page_albums": "Albumy z włączoną kopią zapasową",
"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ń",
@@ -560,29 +563,29 @@
"backup_controller_page_background_turn_off": "Wyłącz usługę w tle",
"backup_controller_page_background_turn_on": "Włącz usługę w tle",
"backup_controller_page_background_wifi": "Tylko Wi-Fi",
"backup_controller_page_backup": "Kopia Zapasowa",
"backup_controller_page_backup": "Kopia zapasowa",
"backup_controller_page_backup_selected": "Zaznaczone: ",
"backup_controller_page_backup_sub": "Skopiowane zdjęcia oraz filmy",
"backup_controller_page_backup_sub": "Zdjęcia i filmy z utworzoną kopią zapasową",
"backup_controller_page_created": "Utworzono dnia: {date}",
"backup_controller_page_desc_backup": "Włącz kopię zapasową, aby automatycznie przesyłać nowe zasoby na serwer.",
"backup_controller_page_desc_backup": "Włącz kopię zapasową na pierwszym planie, aby automatycznie przesyłać nowe zasoby na serwer po otworzeniu aplikacji.",
"backup_controller_page_excluded": "Wykluczone: ",
"backup_controller_page_failed": "Nieudane ({count})",
"backup_controller_page_filename": "Nazwa pliku: {filename} [{size}]",
"backup_controller_page_id": "ID: {id}",
"backup_controller_page_info": "Informacje o kopii zapasowej",
"backup_controller_page_none_selected": "Brak wybranych",
"backup_controller_page_remainder": "Reszta",
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i albumy do wykonania kopii zapasowej z wyboru",
"backup_controller_page_remainder": "Pozostałe",
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i filmy wybrane do wykonania kopii zapasowej",
"backup_controller_page_server_storage": "Pamięć Serwera",
"backup_controller_page_start_backup": "Rozpocznij Kopię Zapasową",
"backup_controller_page_status_off": "Kopia Zapasowa jest wyłaczona",
"backup_controller_page_status_on": "Kopia Zapasowa jest włączona",
"backup_controller_page_storage_format": "{used} z {total} wykorzystanych",
"backup_controller_page_to_backup": "Albumy z Kopią Zapasową",
"backup_controller_page_status_off": "Automatyczne tworzenie kopii zapasowej na pierwszym planie jest wyłączone",
"backup_controller_page_status_on": "Automatyczne tworzenie kopii zapasowej na pierwszym planie jest włączone",
"backup_controller_page_storage_format": "Wykorzystano {used} z {total}",
"backup_controller_page_to_backup": "Albumy, dla których ma być tworzona kopia zapasowa",
"backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów",
"backup_controller_page_turn_off": "Wyłącz Kopię Zapasową",
"backup_controller_page_turn_on": "Włącz Kopię Zapasową",
"backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku",
"backup_controller_page_turn_off": "Wyłącz kopię zapasową na pierwszym planie",
"backup_controller_page_turn_on": "Włącz kopię zapasową na pierwszym planie",
"backup_controller_page_uploading_file_info": "Informacje o przesyłanym pliku",
"backup_err_only_album": "Nie można usunąć jedynego albumu",
"backup_info_card_assets": "zasoby",
"backup_manual_cancelled": "Anulowano",
@@ -590,9 +593,9 @@
"backup_manual_success": "Sukces",
"backup_manual_title": "Stan przesyłania",
"backup_options": "Opcje kopii zapasowej",
"backup_options_page_title": "Opcje kopi zapasowej",
"backup_options_page_title": "Opcje kopii zapasowej",
"backup_setting_subtitle": "Zarządzaj ustawieniami przesyłania w tle i na pierwszym planie",
"backup_settings_subtitle": "Zarządzanie ustawieniami wysyłania",
"backup_settings_subtitle": "Zarządzanie ustawieniami przesyłania",
"backward": "Do tyłu",
"beta_sync": "Status synchronizacji w wersji Beta",
"beta_sync_subtitle": "Zarządzaj nowym systemem synchronizacji",
@@ -773,7 +776,7 @@
"delete_api_key_prompt": "Czy na pewno chcesz usunąć ten klucz API?",
"delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia",
"delete_dialog_alert_local": "Elementy te zostaną trwale usunięte z Twojego urządzenia, ale nadal będą dostępne na serwerze Immich",
"delete_dialog_alert_local_non_backed_up": "Kopia zapasowa niektórych elementów nie jest tworzona w Immich i zostanie trwale usunięta z Twojego urządzenia",
"delete_dialog_alert_local_non_backed_up": "Niektóre elementy nie mają kopii zapasowej w Immich i zostaną trwale usunięte z Twojego urządzenia",
"delete_dialog_alert_remote": "Elementy te zostaną trwale usunięte z serwera Immich",
"delete_dialog_ok_force": "Usuń mimo to",
"delete_dialog_title": "Usuń trwale",
@@ -836,7 +839,7 @@
"downloading": "Pobieranie",
"downloading_asset_filename": "Pobieranie zasobu {filename}",
"downloading_media": "Pobieranie multimediów",
"drop_files_to_upload": "Upuść pliki gdziekolwiek, aby je załadować",
"drop_files_to_upload": "Upuść pliki w dowolnym miejscu, aby je przesłać",
"duplicates": "Duplikaty",
"duplicates_description": "Rozstrzygnij każdą grupę, określając, które zasoby są duplikatami, jeżeli są duplikatami",
"duration": "Czas trwania",
@@ -1073,12 +1076,18 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Ta funkcja , aby działać, ładuje zewnętrzne zasoby z Google.",
"general": "Ogólne",
"geolocation_instruction_all_have_location": "Wszystkie zasoby z tego dnia mają już dane o lokalizacji. Spróbuj wyświetlić wszystkie zasoby lub wybierz inną datę",
"geolocation_instruction_location": "Kliknij na zasób z współrzędnymi GPS, aby użyć jego lokalizacji, lub wybierz lokalizację bezpośrednio z mapy",
"geolocation_instruction_no_date": "Wybierz datę, aby zarządzać danymi o lokalizacji dla zdjęć i filmów z tego dnia",
"geolocation_instruction_no_photos": "Nie znaleziono żadnych zdjęć ani filmów dla tej daty. Wybierz inną datę, aby je wyświetlić",
"get_help": "Pomoc",
"get_wifiname_error": "Nie można uzyskać nazwy Wi-Fi. Upewnij się, że udzieliłeś niezbędnych uprawnień i jesteś połączony z siecią Wi-Fi",
"getting_started": "Pierwsze kroki",
"go_back": "Wstecz",
"go_to_folder": "Idź do folderu",
"go_to_search": "Przejdź do wyszukiwania",
"gps": "GPS",
"gps_missing": "Brak GPS",
"grant_permission": "Udziel pozwolenia",
"group_albums_by": "Grupuj albumy...",
"group_country": "Grupuj według państwa",
@@ -1186,7 +1195,7 @@
"large_files": "Duże pliki",
"last": "Ostatni",
"last_seen": "Ostatnio widziane",
"latest_version": "Najnowsza Wersja",
"latest_version": "Najnowsza wersja",
"latitude": "Szerokość geograficzna",
"leave": "Opuść",
"leave_album": "Opuść album",
@@ -1262,6 +1271,7 @@
"main_branch_warning": "Używasz wersji deweloperskiej. Zdecydowanie zalecamy korzystanie z wydanej wersji aplikacji!",
"main_menu": "Menu główne",
"make": "Marka",
"manage_geolocation": "Zarządzaj lokalizacją",
"manage_shared_links": "Zarządzaj udostępnionymi linkami",
"manage_sharing_with_partners": "Zarządzaj dzieleniem z partnerami",
"manage_the_app_settings": "Zarządzaj ustawieniami aplikacji",
@@ -1417,17 +1427,17 @@
"owner": "Właściciel",
"partner": "Partner",
"partner_can_access": "{partner} ma dostęp do",
"partner_can_access_assets": "Twoje wszystkie zdjęcia i filmy, oprócz tych w Archiwum i Koszu",
"partner_can_access_assets": "Wszystkie Twoje zdjęcia i filmy, oprócz tych w Archiwum i Koszu",
"partner_can_access_location": "Informacji o tym, gdzie zostały zrobione Twoje zdjęcia",
"partner_list_user_photos": "{user} zdjęcia",
"partner_list_user_photos": "Zdjęcia należące do {user}",
"partner_list_view_all": "Pokaż wszystkie",
"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": "{partner} nie będzie już mieć dostępu do twoich zdjęć.",
"partner_sharing": "Dzielenie z Partnerami",
"partner_page_stop_sharing_content": "{partner} nie będzie już mieć dostępu do Twoich zdjęć.",
"partner_sharing": "Dzielenie z partnerami",
"partners": "Partnerzy",
"password": "Hasło",
"password_does_not_match": "Hasła nie są takie same",
@@ -1504,12 +1514,13 @@
"privacy": "Prywatność",
"profile": "Profil",
"profile_drawer_app_logs": "Logi",
"profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji głównej.",
"profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji dodatkowej.",
"profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej głównej wersji.",
"profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej pomniejszej wersji.",
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej wersji głównej.",
"profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej wersji dodatkowej.",
"profile_drawer_readonly_mode": "Włączono tryb tylko do odczytu. Aby wyjść, należy dwukrotnie dotknąć ikony awatara użytkownika.",
"profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej głównej wersji.",
"profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej pomniejszej wersji.",
"profile_image_of_user": "Zdjęcie profilowe {user}",
"profile_picture_set": "Zdjęcie profilowe ustawione.",
"public_album": "Publiczny album",
@@ -1553,6 +1564,8 @@
"rating_description": "Wyświetl ocenę z EXIF w panelu informacji",
"reaction_options": "Opcje reakcji",
"read_changelog": "Zobacz Zmiany",
"readonly_mode_disabled": "Tryb tylko do odczytu wyłączony",
"readonly_mode_enabled": "Tryb tylko do odczytu włączony",
"reassign": "Przypisz ponownie",
"reassigned_assets_to_existing_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do {name, select, null {istniejącej osoby} other {{name}}}",
"reassigned_assets_to_new_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do nowej osoby",
@@ -1722,10 +1735,11 @@
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
"selected": "Zaznaczone",
"selected_count": "{count, plural, other {# wybrane}}",
"selected_gps_coordinates": "wybrane współrzędne GPS",
"send_message": "Wyślij wiadomość",
"send_welcome_email": "Wyślij e-mail powitalny",
"server_endpoint": "Punkt końcowy serwera",
"server_info_box_app_version": "Wersja Aplikacji",
"server_info_box_app_version": "Wersja aplikacji",
"server_info_box_server_url": "Adres URL",
"server_offline": "Serwer Offline",
"server_online": "Serwer Online",
@@ -1832,8 +1846,10 @@
"shift_to_permanent_delete": "naciśnij ⇧, aby trwale usunąć zasób",
"show_album_options": "Pokaż opcje albumu",
"show_albums": "Pokaż albumy",
"show_all_assets": "Pokaż wszystkie zasoby",
"show_all_people": "Pokaż wszystkie osoby",
"show_and_hide_people": "Pokaż lub ukryj osoby",
"show_assets_without_location": "Pokaż zasoby bez lokalizacji",
"show_file_location": "Pokaż ścieżkę pliku",
"show_gallery": "Wyświetl galerię",
"show_hidden_people": "Pokaż ukryte osoby",
@@ -1885,12 +1901,12 @@
"stop_casting": "Zatrzymaj strumieniowanie",
"stop_motion_photo": "Zatrzymaj zdjęcie w ruchu",
"stop_photo_sharing": "Przestać udostępniać swoje zdjęcia?",
"stop_photo_sharing_description": "Od teraz {partner} nie będzie widzieć Twoich zdjęć.",
"stop_photo_sharing_description": "{partner} nie będzie już mieć dostępu do Twoich zdjęć.",
"stop_sharing_photos_with_user": "Przestań udostępniać zdjęcia temu użytkownikowi",
"storage": "Przestrzeń dyskowa",
"storage_label": "Etykieta magazynu",
"storage_quota": "Limit pamięci",
"storage_usage": "{used} z {available} użyte",
"storage_usage": "Wykorzystano {used} z {available}",
"submit": "Zatwierdź",
"success": "Sukces",
"suggestions": "Sugestie",
@@ -1901,7 +1917,7 @@
"swap_merge_direction": "Zmień kierunek złączenia",
"sync": "Synchronizuj",
"sync_albums": "Synchronizuj albumy",
"sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami kopii zapasowych",
"sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami z włączoną kopią zapasową",
"sync_local": "Synchronizacja lokalna",
"sync_remote": "Synchronizacja zdalna",
"sync_upload_album_setting_subtitle": "Twórz i przesyłaj swoje zdjęcia i filmy do wybranych albumów w Immich",
@@ -1946,7 +1962,7 @@
"to_select": "aby wybrać",
"to_trash": "Kosz",
"toggle_settings": "Przełącz ustawienia",
"total": "Całkowity",
"total": "Razem",
"total_usage": "Całkowite wykorzystanie",
"trash": "Kosz",
"trash_action_prompt": "{count} przeniesione do kosza",
@@ -1993,6 +2009,7 @@
"unstacked_assets_count": "{count, plural, one {Rozłożony # zasób} few {Rozłożone # zasoby} other {Rozłożonych # zasobów}}",
"untagged": "Nieoznaczone",
"up_next": "Do następnego",
"update_location_action_prompt": "Zaktualizuj lokalizację {count} wybranych zasobów na:",
"updated_at": "Zaktualizowany",
"updated_password": "Pomyślnie zaktualizowano hasło",
"upload": "Prześlij",
@@ -2017,6 +2034,7 @@
"use_biometric": "Użyj biometrii",
"use_current_connection": "użyj bieżącego połączenia",
"use_custom_date_range": "Zamiast tego użyj niestandardowego zakresu dat",
"use_this_location": "Kliknij, aby użyć lokalizacji",
"user": "Użytkownik",
"user_has_been_deleted": "Ten użytkownik został usunięty.",
"user_id": "ID użytkownika",

View File

@@ -28,6 +28,9 @@
"add_to_album": "Adicionar ao álbum",
"add_to_album_bottom_sheet_added": "Adicionado a {album}",
"add_to_album_bottom_sheet_already_exists": "Já existe em {album}",
"add_to_album_toggle": "Alternar seleção para {album}",
"add_to_albums": "Adicionar aos álbuns",
"add_to_albums_count": "Adicionar aos álbuns ({count})",
"add_to_shared_album": "Adicionar ao álbum partilhado",
"add_url": "Adicionar URL",
"added_to_archive": "Adicionado ao arquivo",
@@ -355,6 +358,9 @@
"trash_number_of_days_description": "Número de dias para manter os ficheiros na reciclagem antes de os eliminar permanentemente",
"trash_settings": "Definições da Reciclagem",
"trash_settings_description": "Gerir definições da reciclagem",
"unlink_all_oauth_accounts": "Desvincular todas as contas OAuth",
"unlink_all_oauth_accounts_description": "Lembre-se de desvincular todas as contas OAuth antes de migrar para um novo provedor.",
"unlink_all_oauth_accounts_prompt": "Tem a certeza de que deseja desvincular todas as contas OAuth? Isso redefinirá o ID OAuth de cada utilizador e não poderá ser desfeito.",
"user_cleanup_job": "Limpeza de utilizadores",
"user_delete_delay": "A conta e os ficheiros de <b>{user}</b> serão agendados para eliminação permanente dentro de {delay, plural, one {# dia} other {# dias}}.",
"user_delete_delay_settings": "Atraso de eliminação",
@@ -390,6 +396,8 @@
"advanced_settings_prefer_remote_title": "Preferir imagens do servidor",
"advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede",
"advanced_settings_proxy_headers_title": "Cabeçalhos do Proxy",
"advanced_settings_readonly_mode_subtitle": "Activa o modo somente leitura, onde as fotos podem ser visualizadas. Recursos como selecionar várias imagens, partilhar, transmitir e excluir ficam deactivados. Activar/Desactivar o modo somente leitura via avatar do utilizador na janela principal",
"advanced_settings_readonly_mode_title": "Modo somente leitura",
"advanced_settings_self_signed_ssl_subtitle": "Não validar o certificado SSL com o endereço do servidor. Isto é necessário para certificados auto-assinados.",
"advanced_settings_self_signed_ssl_title": "Permitir certificados SSL auto-assinados",
"advanced_settings_sync_remote_deletions_subtitle": "Automaticamente eliminar ou restaurar um ficheiro neste dispositivo quando essa mesma ação for efetuada na web",
@@ -455,6 +463,7 @@
"app_bar_signout_dialog_title": "Sair",
"app_settings": "Definições da Aplicação",
"appears_in": "Aparece em",
"apply_count": "Aplicar ({count, number})",
"archive": "Arquivo",
"archive_action_prompt": "{count} adicionados ao Arquivo",
"archive_or_unarchive_photo": "Arquivar ou desarquivar foto",
@@ -494,7 +503,9 @@
"assets": "Ficheiros",
"assets_added_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}}",
"assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum",
"assets_added_to_albums_count": "Adicionado {assetTotal, plural, one {# asset} other {# assets}} a {albumTotal, plural, one {# album} other {# albums}}",
"assets_cannot_be_added_to_album_count": "Não foi possível adicionar {count, plural, one {ficheiro} other {ficheiros}} ao álbum",
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} não pode ser adicionado a nenhum dos álbuns",
"assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}",
"assets_deleted_permanently": "{count} ficheiro(s) eliminado(s) permanentemente",
"assets_deleted_permanently_from_server": "{count} ficheiro(s) eliminado(s) permanentemente do servidor Immich",
@@ -511,6 +522,7 @@
"assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem",
"assets_trashed_from_server": "{count} ficheiro(s) do servidor Immich foi/foram enviados para a reciclagem",
"assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum",
"assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} já faz parte dos álbuns",
"authorized_devices": "Dispositivos Autorizados",
"automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes",
"automatic_endpoint_switching_title": "Troca automática de URL",
@@ -749,6 +761,7 @@
"date_of_birth_saved": "Data de nascimento guardada com sucesso",
"date_range": "Intervalo de datas",
"day": "Dia",
"days": "Dias",
"deduplicate_all": "Remover todos os duplicados",
"deduplication_criteria_1": "Tamanho da imagem em bytes",
"deduplication_criteria_2": "Quantidade de dados EXIF",
@@ -833,10 +846,12 @@
"edit": "Editar",
"edit_album": "Editar álbum",
"edit_avatar": "Editar imagem de perfil",
"edit_birthday": "Alterar aniversário",
"edit_birthday": "Editar aniversário",
"edit_date": "Editar data",
"edit_date_and_time": "Editar data e hora",
"edit_date_and_time_action_prompt": "Alterada a data e hora de {count} ficheiros",
"edit_date_and_time_by_offset": "Alterar data com diferença",
"edit_date_and_time_by_offset_interval": "Novo período: {from} - {to}",
"edit_description": "Editar descrição",
"edit_description_prompt": "Por favor selecione uma nova descrição:",
"edit_exclusion_pattern": "Editar o padrão de exclusão",
@@ -909,6 +924,7 @@
"failed_to_load_notifications": "Ocorreu um erro ao carregar notificações",
"failed_to_load_people": "Ocorreu um erro ao carregar pessoas",
"failed_to_remove_product_key": "Ocorreu um erro ao remover chave de produto",
"failed_to_reset_pin_code": "Falha ao repor o código PIN",
"failed_to_stack_assets": "Ocorreu um erro ao empilhar os ficheiros",
"failed_to_unstack_assets": "Ocorreu um erro ao desempilhar ficheiros",
"failed_to_update_notification_status": "Ocorreu um erro ao atualizar o estado das notificações",
@@ -917,6 +933,7 @@
"paths_validation_failed": "Ocorreu um erro na validação de {paths, plural, one {# caminho} other {# caminhos}}",
"profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixeis transparentes. Por favor amplie e/ou mova a imagem.",
"quota_higher_than_disk_size": "Definiu uma quota maior do que o tamanho do disco",
"something_went_wrong": "Algo deu errado",
"unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum",
"unable_to_add_assets_to_shared_link": "Não foi possível adicionar os ficheiros ao link partilhado",
"unable_to_add_comment": "Não foi possível adicionar o comentário",
@@ -1048,21 +1065,29 @@
"filter_people": "Filtrar pessoas",
"filter_places": "Filtrar lugares",
"find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa",
"first": "Primeiro",
"fix_incorrect_match": "Corrigir correspondência incorreta",
"folder": "Pasta",
"folder_not_found": "Pasta não encontrada",
"folders": "Pastas",
"folders_feature_description": "Navegar na vista de pastas por fotos e vídeos no sistema de ficheiros",
"forgot_pin_code_question": "Esqueceu o seu PIN?",
"forward": "Para a frente",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Esta funcionalidade requer o carregamento de recursos externos da Google para poder funcionar.",
"general": "Geral",
"geolocation_instruction_all_have_location": "Todos os activos desta data já possuem dados de localização. Tente exibir todos os ativos ou seleccione uma data diferente",
"geolocation_instruction_location": "Clique num ativo com coordenadas GPS para usar a sua localização ou seleccione um local diretamente do mapa",
"geolocation_instruction_no_date": "Seleccione uma data para gerir os dados de localização de fotos e vídeos daquele dia",
"geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado para esta data. Seleccione uma data diferente para exibi-los",
"get_help": "Obter Ajuda",
"get_wifiname_error": "Não foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissões necessárias e se está conectado a uma rede Wi-Fi",
"getting_started": "Primeiros Passos",
"go_back": "Regressar",
"go_to_folder": "Ir para a pasta",
"go_to_search": "Ir para a pesquisa",
"gps": "GPS",
"gps_missing": "Sem GPS",
"grant_permission": "Conceder permissão",
"group_albums_by": "Agrupar álbuns por...",
"group_country": "Agrupar por país",
@@ -1107,6 +1132,7 @@
"home_page_upload_err_limit": "Só é possível enviar 30 arquivos por vez, ignorando",
"host": "Servidor",
"hour": "Hora",
"hours": "Horas",
"id": "ID",
"idle": "Em espera",
"ignore_icloud_photos": "ignorar fotos no iCloud",
@@ -1167,6 +1193,7 @@
"language_search_hint": "Procurar línguas...",
"language_setting_description": "Selecione o seu Idioma preferido",
"large_files": "Ficheiros Grandes",
"last": "Último",
"last_seen": "Visto pela ultima vez",
"latest_version": "Versão mais recente",
"latitude": "Latitude",
@@ -1185,6 +1212,7 @@
"library_page_sort_title": "Título do álbum",
"licenses": "Licenças",
"light": "Claro",
"like": "Gostar",
"like_deleted": "Gosto removido",
"link_motion_video": "Relacionar video animado",
"link_to_oauth": "Link do OAuth",
@@ -1243,6 +1271,7 @@
"main_branch_warning": "Está a utilizar uma versão de desenvolvimento, recomendamos vivamente que utilize uma versão estável!",
"main_menu": "Menu Principal",
"make": "Marca",
"manage_geolocation": "Gerir localização",
"manage_shared_links": "Gerir links partilhados",
"manage_sharing_with_partners": "Gerir partilha com parceiros",
"manage_the_app_settings": "Gerir definições da aplicação",
@@ -1251,7 +1280,7 @@
"manage_your_devices": "Gerir os seus dispositivos com sessão iniciada",
"manage_your_oauth_connection": "Gerir a sua ligação ao OAuth",
"map": "Mapa",
"map_assets_in_bounds": "{count, plural, one {# foto} other {# fotos}}",
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
"map_cannot_get_user_location": "Impossível obter a sua localização",
"map_location_dialog_yes": "Sim",
"map_location_picker_page_use_location": "Utilizar esta localização",
@@ -1295,6 +1324,7 @@
"merged_people_count": "Unidas {count, plural, one {# pessoa} other {# pessoas}}",
"minimize": "Minimizar",
"minute": "Minuto",
"minutes": "Minutos",
"missing": "Em falta",
"model": "Modelo",
"month": "Mês",
@@ -1314,6 +1344,9 @@
"my_albums": "Os meus álbuns",
"name": "Nome",
"name_or_nickname": "Nome ou alcunha",
"network_requirement_photos_upload": "Usar dados móveis para fazer backup de fotos",
"network_requirement_videos_upload": "Usar dados móveis para fazer backup de vídeos",
"network_requirements_updated": "Requisitos de rede alterados, redefinindo fila de backup",
"networking_settings": "Conexões",
"networking_subtitle": "Gerencie a conexão do servidor",
"never": "Nunca",
@@ -1365,6 +1398,7 @@
"oauth": "OAuth",
"official_immich_resources": "Recursos oficiais do Immich",
"offline": "Offline",
"offset": "Desvio",
"ok": "Ok",
"oldest_first": "Mais antigo primeiro",
"on_this_device": "Neste dispositivo",
@@ -1442,6 +1476,9 @@
"permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça backups e gerencie sua galeria, conceda permissões para fotos e vídeos nas configurações.",
"permission_onboarding_request": "O Immich requer autorização para ver as suas fotos e vídeos.",
"person": "Pessoa",
"person_age_months": "{months, plural, one {# month} other {# months}} idade",
"person_age_year_months": "1 ano, {months, plural, one {# month} other {# months}} idade",
"person_age_years": "{years, plural, other {# years}} idade",
"person_birthdate": "Nasceu a {date}",
"person_hidden": "{name}{hidden, select, true { (oculto)} other {}}",
"photo_shared_all_users": "Parece que partilhou as suas fotos com todos os utilizadores ou não tem nenhum utilizador para partilhar.",
@@ -1481,6 +1518,7 @@
"profile_drawer_client_out_of_date_minor": "O aplicativo está desatualizado. Por favor, atualize para a versão mais recente.",
"profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Modo somente leitura activado. Toque duas vezes no ícone do avatar do utilizador para sair.",
"profile_drawer_server_out_of_date_major": "O servidor está desatualizado. Atualize para a versão principal mais recente.",
"profile_drawer_server_out_of_date_minor": "O servidor está desatualizado. Atualize para a versão mais recente.",
"profile_image_of_user": "Imagem de perfil de {user}",
@@ -1526,6 +1564,8 @@
"rating_description": "Mostrar a classificação EXIF no painel de informações",
"reaction_options": "Opções de reação",
"read_changelog": "Ler Novidades",
"readonly_mode_disabled": "Modo somente leitura desactivado",
"readonly_mode_enabled": "Modo somente leitura activado",
"reassign": "Reatribuir",
"reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# ficheiro} other {# ficheiros}} para {name, select, null {uma pessoa existente} other {{name}}}",
"reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa",
@@ -1587,6 +1627,9 @@
"reset_password": "Redefinir palavra-passe",
"reset_people_visibility": "Redefinir pessoas ocultas",
"reset_pin_code": "Repor código PIN",
"reset_pin_code_description": "Se esqueceu o seu código PIN, pode entrar em contato com o administrador do servidor para o repor",
"reset_pin_code_success": "Código PIN redefinido com sucesso",
"reset_pin_code_with_password": "Pode sempre repor o seu código PIN com a sua senha",
"reset_sqlite": "Reiniciar Base de Dados SQLite",
"reset_sqlite_confirmation": "Tem a certeza de que quer reiniciar a base de dados SQLite? Vai ter de terminar a sessão e entrar outra vez para sincronizar os dados de novo",
"reset_sqlite_success": "Base de dados SQLite reiniciada com sucesso",
@@ -1601,6 +1644,7 @@
"resume": "Continuar",
"retry_upload": "Tentar carregar novamente",
"review_duplicates": "Rever itens duplicados",
"review_large_files": "Rever arquivos grandes",
"role": "Função",
"role_editor": "Editor",
"role_viewer": "Visualizador",
@@ -1691,6 +1735,7 @@
"select_user_for_sharing_page_err_album": "Ocorreu um erro ao criar o álbum",
"selected": "Selecionados",
"selected_count": "{count, plural, other {# selecionados}}",
"selected_gps_coordinates": "coordenadas gps seleccionadas",
"send_message": "Enviar mensagem",
"send_welcome_email": "Enviar E-mail de boas vindas",
"server_endpoint": "URL do servidor",
@@ -1758,6 +1803,7 @@
"shared_link_clipboard_copied_massage": "Copiado para a área de transferência",
"shared_link_clipboard_text": "Ligação: {link}\nPalavra-passe: {password}",
"shared_link_create_error": "Erro ao criar o link compartilhado",
"shared_link_custom_url_description": "Aceda a este link partilhado com um URL personalizado",
"shared_link_edit_description_hint": "Digite a descrição do compartilhamento",
"shared_link_edit_expire_after_option_day": "1 dia",
"shared_link_edit_expire_after_option_days": "{count} dias",
@@ -1783,6 +1829,7 @@
"shared_link_info_chip_metadata": "EXIF",
"shared_link_manage_links": "Gerenciar links compartilhados",
"shared_link_options": "Opções de link partilhado",
"shared_link_password_description": "Exigir uma senha para aceder a este link partilhado",
"shared_links": "Links partilhados",
"shared_links_description": "Partilhar fotos e videos com um link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos partilhados.}}",
@@ -1799,8 +1846,10 @@
"shift_to_permanent_delete": "Pressione ⇧ para eliminar o ficheiro permanentemente",
"show_album_options": "Exibir opções do álbum",
"show_albums": "Mostrar álbuns",
"show_all_assets": "Mostrar todos os recursos",
"show_all_people": "Mostrar todas as pessoas",
"show_and_hide_people": "Mostrar & ocultar pessoas",
"show_assets_without_location": "Mostrar recursos sem localização",
"show_file_location": "Exibir localização do ficheiro",
"show_gallery": "Exibir galeria",
"show_hidden_people": "Exibir pessoas ocultadas",
@@ -1832,6 +1881,7 @@
"sort_created": "Data de criação",
"sort_items": "Número de itens",
"sort_modified": "Data de modificação",
"sort_newest": "A foto mais recente",
"sort_oldest": "Foto mais antiga",
"sort_people_by_similarity": "Ordenar pessoas por semelhança",
"sort_recent": "Foto mais recente",
@@ -1907,7 +1957,9 @@
"to_change_password": "Alterar palavra-passe",
"to_favorite": "Favorito",
"to_login": "Iniciar Sessão",
"to_multi_select": "multi-selecção",
"to_parent": "Subir um nível",
"to_select": "seleccionar",
"to_trash": "Reciclagem",
"toggle_settings": "Alternar configurações",
"total": "Total",
@@ -1957,6 +2009,7 @@
"unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}",
"untagged": "Marcador removido",
"up_next": "A seguir",
"update_location_action_prompt": "Actualize a localização de {count} activos seleccionados com:",
"updated_at": "Atualizado a",
"updated_password": "Palavra-passe atualizada",
"upload": "Carregar",
@@ -1981,6 +2034,7 @@
"use_biometric": "Utilizar dados biométricos",
"use_current_connection": "usar conexão atual",
"use_custom_date_range": "Utilizar um intervalo de datas personalizado",
"use_this_location": "Clique para usar a localização",
"user": "Utilizador",
"user_has_been_deleted": "Este utilizador for eliminado.",
"user_id": "ID do utilizador",

View File

@@ -1076,8 +1076,8 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "Esta funcionalidade carrega recursos externos do Google para funcionar.",
"general": "Geral",
"geolocation_instruction_all_have_location": "Todos arquivos nesta data já contem dados de localização. Tente exibir todos os arquivos ou selecione uma data diferente",
"geolocation_instruction_location": "Selecione o arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa",
"geolocation_instruction_all_have_location": "Todos arquivos nesta data já possuem dados de localização. Tente exibir todos os arquivos ou selecione uma data diferente",
"geolocation_instruction_location": "Selecione um arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa",
"geolocation_instruction_no_date": "Selecione uma data para gerenciar os dados de localização das fotos e vídeos daquele dia",
"geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado nesta data. Selecione uma data diferente para ser exibida",
"get_help": "Obter Ajuda",

View File

@@ -28,6 +28,9 @@
"add_to_album": "Adaugă în album",
"add_to_album_bottom_sheet_added": "Adăugat în {album}",
"add_to_album_bottom_sheet_already_exists": "Deja în {album}",
"add_to_album_toggle": "Selectează/deselectează {album}",
"add_to_albums": "Adaugă la albume",
"add_to_albums_count": "Adaugă la albume ({count})",
"add_to_shared_album": "Adaugă la album partajat",
"add_url": "Adăugați adresa URL",
"added_to_archive": "Adăugat la arhivă",
@@ -181,6 +184,7 @@
"nightly_tasks_generate_memories_setting": "Generare memorii",
"nightly_tasks_generate_memories_setting_description": "Creează amintiri noi din resurse",
"nightly_tasks_missing_thumbnails_setting": "Generează miniaturi lipsă",
"nightly_tasks_missing_thumbnails_setting_description": "Pune în coadă elementele fără miniaturi pentru generarea miniaturilor",
"nightly_tasks_settings": "Setări pentru sarcinile nocturne",
"nightly_tasks_settings_description": "Gestionați sarcinile nocturne",
"nightly_tasks_start_time_setting": "Ora de începere",
@@ -354,6 +358,9 @@
"trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ",
"trash_settings": "Setǎri Coș de Gunoi",
"trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi",
"unlink_all_oauth_accounts": "Deconectează toate conturile OAuth",
"unlink_all_oauth_accounts_description": "Nu uita să deconectezi toate conturile OAuth înainte de a migra la un nou furnizor.",
"unlink_all_oauth_accounts_prompt": "Ești sigur că vrei să deconectezi toate conturile OAuth? Aceasta va reseta ID-ul OAuth pentru fiecare utilizator și nu poate fi anulată.",
"user_cleanup_job": "Curățare utilizator",
"user_delete_delay": "Contul și resursele utilizatorului <b>{user}</b> vor fi programate pentru ștergere permanentă în {delay, plural, one {# zi} other {# zile}}.",
"user_delete_delay_settings": "Întârziere la ștergere",
@@ -389,6 +396,8 @@
"advanced_settings_prefer_remote_title": "Preferă fotografii la distanță",
"advanced_settings_proxy_headers_subtitle": "Definește antetele proxy pe care Immich ar trebui să le trimită cu fiecare solicitare de rețea",
"advanced_settings_proxy_headers_title": "Antete Proxy",
"advanced_settings_readonly_mode_subtitle": "Activează modul doar-citire, în care fotografiile pot fi doar vizualizate, iar acțiuni precum selectarea mai multor imagini, partajarea, redarea pe alt dispozitiv sau ștergerea sunt dezactivate. Activează/Dezactivează modul doar-citire din avatarul utilizatorului de pe ecranul principal.",
"advanced_settings_readonly_mode_title": "Mod doar-citire",
"advanced_settings_self_signed_ssl_subtitle": "Omite verificare certificate SSL pentru distinația server-ului, necesar pentru certificate auto-semnate.",
"advanced_settings_self_signed_ssl_title": "Permite certificate SSL auto-semnate",
"advanced_settings_sync_remote_deletions_subtitle": "Ștergeți sau restaurați automat un element de pe acest dispozitiv atunci când acțiunea este efectuată pe web",
@@ -454,6 +463,7 @@
"app_bar_signout_dialog_title": "Deconectare",
"app_settings": "Setări Aplicație",
"appears_in": "Apare în",
"apply_count": "Aplică ({count, number})",
"archive": "Arhivă",
"archive_action_prompt": "{count} adăugate la Arhivă",
"archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia",
@@ -493,7 +503,9 @@
"assets": "Resurse",
"assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}",
"assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în album",
"assets_added_to_albums_count": "Au fost adăugate {assetTotal, plural, one {# element} other {# elemente}} la {albumTotal, plural, one {# album} other {# albume}}",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} nu pot fi adăugate în album",
"assets_cannot_be_added_to_albums": "{count, plural, one {Elementul} other {Elementele}} nu poate fi adăugat la niciunul dintre albume",
"assets_count": "{count, plural, one {# resursă} other {# resurse}}",
"assets_deleted_permanently": "{count} poză/poze ștearsă/șterse permanent",
"assets_deleted_permanently_from_server": "{count} poză/poze ștearsă/șterse permanent din serverul Immich",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Открыть фильтры поиска",
"options": "Опции",
"or": "или",
"organize_into_albums": "Распределить по альбомам",
"organize_into_albums_description": "Добавить уже существующие фотографии в альбомы, используя текущие настройки синхронизации",
"organize_your_library": "Приведите в порядок свою библиотеку",
"original": "оригинал",
"other": "Другое",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Состояние поддержки",
"purchase_server_title": "Сервер",
"purchase_settings_server_activated": "Ключом продукта управляет администратор сервера",
"query_asset_id": "Идентификатор исходного объекта",
"queue_status": "В очереди {count}/{total}",
"rating": "Рейтинг звёзд",
"rating_clear": "Очистить рейтинг",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Не удалось создать альбом",
"selected": "Выбрано",
"selected_count": "{count, plural, one {Выбран # объект} many {Выбрано # объектов} other {Выбрано # объекта}}",
"selected_gps_coordinates": "выбранные координаты",
"selected_gps_coordinates": "Выбранные координаты",
"send_message": "Отправить сообщение",
"send_welcome_email": "Отправить приветственное письмо",
"server_endpoint": "Адрес сервера",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Показать следующий объект",
"view_previous_asset": "Показать предыдущий объект",
"view_qr_code": "Посмотреть QR код",
"view_similar_photos": "Найти похожие фотографии",
"view_stack": "Показать группу",
"view_user": "Просмотреть пользователя",
"viewer_remove_from_stack": "Убрать из группы",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Otvoriť vyhľadávacie filtre",
"options": "Nastavenia",
"or": "alebo",
"organize_into_albums": "Usporiadať do albumov",
"organize_into_albums_description": "Vložiť existujúce fotky do albumov podľa aktuálnych nastavení synchronizácie",
"organize_your_library": "Usporiadajte svoju knižnicu",
"original": "originál",
"other": "Ostatné",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Štatút podporovateľa",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktový kľúč servera spravuje admin",
"query_asset_id": "ID požiadavky položky",
"queue_status": "V poradí {count}/{total}",
"rating": "Hodnotenie hviezdičkami",
"rating_clear": "Vyčistiť hodnotenie",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album",
"selected": "Vybrané",
"selected_count": "{count, plural, one {# vybraná} few {# vybrané} other {# vybraných}}",
"selected_gps_coordinates": "vybrané GPS súradnice",
"selected_gps_coordinates": "Vybrané GPS súradnice",
"send_message": "Odoslať správu",
"send_welcome_email": "Odoslať uvítací e-mail",
"server_endpoint": "Koncový bod servera",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Zobraziť nasledujúci súbor",
"view_previous_asset": "Zobraziť predchádzajúci súbor",
"view_qr_code": "Zobraziť QR kód",
"view_similar_photos": "Zobraziť podobné fotografie",
"view_stack": "Zobraziť zoskupenie",
"view_user": "Zobraziť používateľa",
"viewer_remove_from_stack": "Odstrániť zo zoskupenia",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Odpri iskalne filtre",
"options": "Možnosti",
"or": "ali",
"organize_into_albums": "Organiziraj v albume",
"organize_into_albums_description": "Dodaj obstoječe fotografije v albume z uporabo trenutnih nastavitev sinhronizacije",
"organize_your_library": "Organiziraj svojo knjižnico",
"original": "izvirnik",
"other": "drugo",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Status podpornika",
"purchase_server_title": "Strežnik",
"purchase_settings_server_activated": "Ključ izdelka strežnika upravlja skrbnik",
"query_asset_id": "ID sredstva poizvedbe",
"queue_status": "Čakalna vrsta {count}/{total}",
"rating": "Ocena z zvezdicami",
"rating_clear": "Počisti oceno",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Ogled naslednjega sredstva",
"view_previous_asset": "Ogled prejšnjega sredstva",
"view_qr_code": "Oglej si kodo QR",
"view_similar_photos": "Oglejte si podobne fotografije",
"view_stack": "Ogled sklada",
"view_user": "Poglej uporabnika",
"viewer_remove_from_stack": "Odstrani iz sklada",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Öppna sökfilter",
"options": "Val",
"or": "eller",
"organize_into_albums": "Organisera i album",
"organize_into_albums_description": "Lägg befintliga foton i album med aktuella synkroniseringsinställningar",
"organize_your_library": "Organisera ditt bibliotek",
"original": "original",
"other": "Övrigt",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Supporterstatus",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Produktnyckeln för servern hanteras av administratören",
"query_asset_id": "Fråga om objekts-ID",
"queue_status": "Köande {count}/{total}",
"rating": "Antal stjärnor",
"rating_clear": "Ta bort betyg",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album",
"selected": "Valda",
"selected_count": "{count, plural, other {# valda}}",
"selected_gps_coordinates": "valda GPS-koordinater",
"selected_gps_coordinates": "Valda GPS-koordinater",
"send_message": "Skicka meddelande",
"send_welcome_email": "Skicka välkomstmejl",
"server_endpoint": "Server-endpoint",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Visa nästa objekt",
"view_previous_asset": "Visa föregående objekt",
"view_qr_code": "Visa QR-kod",
"view_similar_photos": "Visa liknande foton",
"view_stack": "Visa Stapel",
"view_user": "Visa Användare",
"viewer_remove_from_stack": "Ta bort från Stapeln",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "Відкрийте фільтри пошуку",
"options": "Налаштування",
"or": "або",
"organize_into_albums": "Упорядкувати в альбоми",
"organize_into_albums_description": "Помістити наявні фотографії в альбоми, використовуючи поточні налаштування синхронізації",
"organize_your_library": "Організуйте свою бібліотеку",
"original": "оригінал",
"other": "Інше",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "Статус підтримки",
"purchase_server_title": "Сервер",
"purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором",
"query_asset_id": "Ідентифікатор ресурсу запиту",
"queue_status": "У черзі {count} з {total}",
"rating": "Зоряний рейтинг",
"rating_clear": "Очистити рейтинг",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "Не вдалося створити альбом",
"selected": "Обрано",
"selected_count": "{count, plural, one {# обраний} other {# обраних}}",
"selected_gps_coordinates": "вибрані GPS-координати",
"selected_gps_coordinates": "Вибрані GPS-координати",
"send_message": "Надіслати повідомлення",
"send_welcome_email": "Надішліть вітальний лист",
"server_endpoint": "Кінцева точка сервера",
@@ -2077,6 +2080,7 @@
"view_next_asset": "Переглянути наступний ресурс",
"view_previous_asset": "Переглянути попередній ресурс",
"view_qr_code": "Переглянути QR-код",
"view_similar_photos": "Переглянути схожі фотографії",
"view_stack": "Перегляд стеку",
"view_user": "Переглянути користувача",
"viewer_remove_from_stack": "Видалити зі стеку",

View File

@@ -396,6 +396,8 @@
"advanced_settings_prefer_remote_title": "偏好遠端影像",
"advanced_settings_proxy_headers_subtitle": "定義 Immich 在每次網路請求時應該發送的代理標頭",
"advanced_settings_proxy_headers_title": "代理標頭",
"advanced_settings_readonly_mode_subtitle": "開啟唯讀模式後,照片只能瀏覽,像是多選影像、分享、投放、刪除等功能都會關閉。可在主畫面透過使用者頭像來開啟/關閉唯讀模式",
"advanced_settings_readonly_mode_title": "唯讀模式",
"advanced_settings_self_signed_ssl_subtitle": "略過伺服器端點的 SSL 憑證驗證。自簽憑證時必須啟用此設定。",
"advanced_settings_self_signed_ssl_title": "允許自簽的 SSL 憑證",
"advanced_settings_sync_remote_deletions_subtitle": "當在網頁端執行刪除或還原操作時,自動在此裝置上刪除或還原該媒體",
@@ -1073,6 +1075,10 @@
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "此功能需要從 Google 載入外部資源才能正常運作。",
"general": "一般",
"geolocation_instruction_all_have_location": "此日期的所有項目已具有位置資料。請嘗試顯示所有項目或選擇其他日期",
"geolocation_instruction_location": "點擊具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點",
"geolocation_instruction_no_date": "選擇日期以管理該天的照片和影片位置資料",
"geolocation_instruction_no_photos": "此日期沒有找到照片或影片。請選擇其他日期以顯示",
"get_help": "線上求助",
"get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連接至 Wi-Fi 網路",
"getting_started": "開始使用",
@@ -1262,6 +1268,7 @@
"main_branch_warning": "您現在使用的是開發版本;我們強烈您建議使用正式發行版!",
"main_menu": "主頁面",
"make": "製造商",
"manage_geolocation": "管理位置",
"manage_shared_links": "管理共享連結",
"manage_sharing_with_partners": "管理與親朋好友的分享",
"manage_the_app_settings": "管理應用程式設定",
@@ -1508,6 +1515,7 @@
"profile_drawer_client_out_of_date_minor": "客戶端有小版本升級,請盡快升級至最新版。",
"profile_drawer_client_server_up_to_date": "客戶端和服務端都是最新的",
"profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "唯讀模式已開啟。請連點兩下使用者頭像圖示以退出。",
"profile_drawer_server_out_of_date_major": "服務端有大版本升級,請盡快升級至最新版。",
"profile_drawer_server_out_of_date_minor": "服務端有小版本升級,請盡快升級至最新版。",
"profile_image_of_user": "{user} 的個人資料圖片",
@@ -1553,6 +1561,8 @@
"rating_description": "在資訊面板中顯示 EXIF 評等",
"reaction_options": "反應選項",
"read_changelog": "閱覽變更日誌",
"readonly_mode_disabled": "唯讀模式已關閉",
"readonly_mode_enabled": "唯讀模式已開啟",
"reassign": "重新指定",
"reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個檔案}}重新指定給{name, select, null {現有的人} other {{name}}}",
"reassigned_assets_to_new_person": "已將 {count, plural, other {# 個檔案}}重新指定給一位新人物",
@@ -1722,6 +1732,7 @@
"select_user_for_sharing_page_err_album": "新增相簿失敗",
"selected": "已選擇",
"selected_count": "{count, plural, other {選了 # 項}}",
"selected_gps_coordinates": "已選擇的 GPS 座標",
"send_message": "傳訊息",
"send_welcome_email": "傳送歡迎電子郵件",
"server_endpoint": "伺服器端點",
@@ -1832,8 +1843,10 @@
"shift_to_permanent_delete": "按 ⇧ 永久刪除檔案",
"show_album_options": "顯示相簿選項",
"show_albums": "顯示相簿",
"show_all_assets": "顯示所有項目",
"show_all_people": "顯示所有人物",
"show_and_hide_people": "顯示與隱藏人物",
"show_assets_without_location": "顯示沒有地點的項目",
"show_file_location": "顯示文件位置",
"show_gallery": "顯示畫廊",
"show_hidden_people": "顯示隱藏的人物",
@@ -2015,6 +2028,7 @@
"use_biometric": "使用生物辨識",
"use_current_connection": "使用目前的連線",
"use_custom_date_range": "改用自訂日期範圍",
"use_this_location": "點擊以使用位置",
"user": "使用者",
"user_has_been_deleted": "此用戶已被刪除。",
"user_id": "使用者 ID",

View File

@@ -1417,6 +1417,8 @@
"open_the_search_filters": "打开搜索过滤器",
"options": "选项",
"or": "或",
"organize_into_albums": "整理成相册",
"organize_into_albums_description": "使用当前同步设置将现有照片放入相册",
"organize_your_library": "整理您的图库",
"original": "原图",
"other": "其它",
@@ -1557,6 +1559,7 @@
"purchase_server_description_2": "支持者状态",
"purchase_server_title": "服务器",
"purchase_settings_server_activated": "服务器产品密钥正在由管理员管理",
"query_asset_id": "查询资产ID",
"queue_status": "排队中 {count}/{total}",
"rating": "星级",
"rating_clear": "删除星级",
@@ -1735,7 +1738,7 @@
"select_user_for_sharing_page_err_album": "创建相册失败",
"selected": "已选择",
"selected_count": "{count, plural, other {#项已选择}}",
"selected_gps_coordinates": "选定的GPS坐标",
"selected_gps_coordinates": "选定的GPS坐标",
"send_message": "发送消息",
"send_welcome_email": "发送欢迎邮件",
"server_endpoint": "服务器 URL",
@@ -2077,6 +2080,7 @@
"view_next_asset": "查看下一项",
"view_previous_asset": "查看上一项",
"view_qr_code": "查看二维码",
"view_similar_photos": "查看相似照片",
"view_stack": "查看堆叠项目",
"view_user": "查看用户",
"viewer_remove_from_stack": "从堆叠中移除",

View File

@@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
RUN apt-get update && apt-get install -y --no-install-recommends g++
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:f3660c56d5b08d6c516360981bedc439f499b9bf37f46a216018da3777a74011 /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:0.8.15@sha256:a5727064a0de127bdb7c9d3c1383f3a9ac307d9f2d8a391edc7896c54289ced0 /uv /uvx /bin/
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \

34
mise.lock Normal file
View File

@@ -0,0 +1,34 @@
[tools.dart]
version = "3.8.2"
backend = "asdf:dart"
[tools.flutter]
version = "3.32.8-stable"
backend = "asdf:flutter"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.31.4"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64]
checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955"
size = 8821083
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip"
[tools.node]
version = "22.18.0"
backend = "core:node"
[tools.node.platforms.linux-x64]
checksum = "sha256:a2e703725d8683be86bb5da967bf8272f4518bdaf10f21389e2b2c9eaeae8c8a"
size = 54824343
url = "https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz"
[tools.pnpm]
version = "10.14.0"
backend = "aqua:pnpm/pnpm"
[tools.pnpm.platforms.linux-x64]
checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d"
size = 66231525
url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64"

312
mise.toml Normal file
View File

@@ -0,0 +1,312 @@
[tools]
node = "22.19.0"
flutter = "3.32.8"
pnpm = "10.14.0"
dart = "3.8.2"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.31.4"
bin = "dcm"
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
[settings]
experimental = true
lockfile = true
pin = true
# .github
[tasks."github:install"]
run = "pnpm install --filter github --frozen-lockfile"
[tasks."github:format"]
env._.path = "./.github/node_modules/.bin"
dir = ".github"
run = "prettier --check ."
[tasks."github:format-fix"]
env._.path = "./.github/node_modules/.bin"
dir = ".github"
run = "prettier --write ."
# @immich/cli
[tasks."cli:install"]
run = "pnpm install --filter @immich/cli --frozen-lockfile"
[tasks."cli:build"]
env._.path = "./cli/node_modules/.bin"
dir = "cli"
run = "vite build"
[tasks."cli:test"]
env._.path = "./cli/node_modules/.bin"
dir = "cli"
run = "vite"
[tasks."cli:lint"]
env._.path = "./cli/node_modules/.bin"
dir = "cli"
run = "eslint \"src/**/*.ts\" --max-warnings 0"
[tasks."cli:lint-fix"]
run = "mise run cli:lint --fix"
[tasks."cli:format"]
env._.path = "./cli/node_modules/.bin"
dir = "cli"
run = "prettier --check ."
[tasks."cli:format-fix"]
env._.path = "./cli/node_modules/.bin"
dir = "cli"
run = "prettier --write ."
[tasks."cli:check"]
env._.path = "./cli/node_modules/.bin"
dir = "cli"
run = "tsc --noEmit"
# @immich/sdk
[tasks."sdk:install"]
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
[tasks."sdk:build"]
env._.path = "./open-api/typescript-sdk/node_modules/.bin"
dir = "./open-api/typescript-sdk"
run = "tsc"
# docs
[tasks."docs:install"]
run = "pnpm install --filter documentation --frozen-lockfile"
[tasks."docs:start"]
env._.path = "./docs/node_modules/.bin"
dir = "docs"
run = "docusaurus --port 3005"
[tasks."docs:build"]
env._.path = "./docs/node_modules/.bin"
dir = "docs"
run = [
"jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
"docusaurus build",
]
[tasks."docs:preview"]
env._.path = "./docs/node_modules/.bin"
dir = "docs"
run = "docusaurus serve"
[tasks."docs:format"]
env._.path = "./docs/node_modules/.bin"
dir = "docs"
run = "prettier --check ."
[tasks."docs:format-fix"]
env._.path = "./docs/node_modules/.bin"
dir = "docs"
run = "prettier --write ."
# e2e
[tasks."e2e:install"]
run = "pnpm install --filter immich-e2e --frozen-lockfile"
[tasks."e2e:test"]
env._.path = "./e2e/node_modules/.bin"
dir = "e2e"
run = "vitest --run"
[tasks."e2e:test-web"]
env._.path = "./e2e/node_modules/.bin"
dir = "e2e"
run = "playwright test"
[tasks."e2e:format"]
env._.path = "./e2e/node_modules/.bin"
dir = "e2e"
run = "prettier --check ."
[tasks."e2e:format-fix"]
env._.path = "./e2e/node_modules/.bin"
dir = "e2e"
run = "prettier --write ."
[tasks."e2e:lint"]
env._.path = "./e2e/node_modules/.bin"
dir = "e2e"
run = "eslint \"src/**/*.ts\" --max-warnings 0"
[tasks."e2e:lint-fix"]
run = "mise run e2e:lint --fix"
[tasks."e2e:check"]
env._.path = "./e2e/node_modules/.bin"
dir = "e2e"
run = "tsc --noEmit"
# i18n
[tasks."i18n:format"]
run = "mise run i18n:format-fix"
[tasks."i18n:format-fix"]
run = "pnpm dlx sort-json ./i18n/*.json"
# server
[tasks."server:install"]
run = "pnpm install --filter immich --frozen-lockfile"
[tasks."server:build"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "nest build"
[tasks."server:test"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "vitest --config test/vitest.config.mjs"
[tasks."server:test-medium"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "vitest --config test/vitest.config.medium.mjs"
[tasks."server:format"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "prettier --check ."
[tasks."server:format-fix"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "prettier --write ."
[tasks."server:lint"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0"
[tasks."server:lint-fix"]
run = "mise run server:lint --fix"
[tasks."server:check"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "tsc --noEmit"
[tasks."server:sql"]
dir = "server"
run = "node ./dist/bin/sync-open-api.js"
[tasks."server:open-api"]
dir = "server"
run = "node ./dist/bin/sync-open-api.js"
[tasks."server:migrations"]
dir = "server"
run = "node ./dist/bin/migrations.js"
description = "Run database migration commands (create, generate, run, debug, or query)"
[tasks."server:schema-drop"]
run = "mise run server:migrations query 'DROP schema public cascade; CREATE schema public;'"
[tasks."server:schema-reset"]
run = "mise run server:schema-drop && mise run server:migrations run"
[tasks."server:email-dev"]
env._.path = "./server/node_modules/.bin"
dir = "server"
run = "email dev -p 3050 --dir src/emails"
[tasks."server:checklist"]
run = [
"mise run server:install",
"mise run server:format",
"mise run server:lint",
"mise run server:check",
"mise run server:test-medium --run",
"mise run server:test --run",
]
# web
[tasks."web:install"]
run = "pnpm install --filter immich-web --frozen-lockfile"
[tasks."web:svelte-kit-sync"]
env._.path = "./web/node_modules/.bin"
dir = "web"
run = "svelte-kit sync"
[tasks."web:build"]
env._.path = "./web/node_modules/.bin"
dir = "web"
run = "vite build"
[tasks."web:build-stats"]
env.BUILD_STATS = "true"
env._.path = "./web/node_modules/.bin"
dir = "web"
run = "vite build"
[tasks."web:preview"]
env._.path = "./web/node_modules/.bin"
dir = "web"
run = "vite preview"
[tasks."web:start"]
env._.path = "web/node_modules/.bin"
dir = "web"
run = "vite dev --host 0.0.0.0 --port 3000"
[tasks."web:test"]
depends = "web:svelte-kit-sync"
env._.path = "web/node_modules/.bin"
dir = "web"
run = "vitest"
[tasks."web:format"]
env._.path = "web/node_modules/.bin"
dir = "web"
run = "prettier --check ."
[tasks."web:format-fix"]
env._.path = "web/node_modules/.bin"
dir = "web"
run = "prettier --write ."
[tasks."web:lint"]
env._.path = "web/node_modules/.bin"
dir = "web"
run = "eslint . --max-warnings 0"
[tasks."web:lint-p"]
env._.path = "web/node_modules/.bin"
dir = "web"
run = "eslint-p . --max-warnings 0 --concurrency=4"
[tasks."web:lint-fix"]
run = "mise run web:lint --fix"
[tasks."web:check"]
depends = "web:svelte-kit-sync"
env._.path = "web/node_modules/.bin"
dir = "web"
run = "tsc --noEmit"
[tasks."web:check-svelte"]
depends = "web:svelte-kit-sync"
env._.path = "web/node_modules/.bin"
dir = "web"
run = "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte"
[tasks."web:checklist"]
run = [
"mise run web:install",
"mise run web:format",
"mise run web:check",
"mise run web:test --run",
"mise run web:lint",
]

View File

@@ -61,9 +61,8 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerFgHostApi {
fun enableSyncWorker()
fun enableUploadWorker(callbackHandle: Long)
fun disableUploadWorker()
fun enable()
fun disable()
companion object {
/** The codec used by BackgroundWorkerFgHostApi. */
@@ -75,11 +74,11 @@ interface BackgroundWorkerFgHostApi {
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$separatedMessageChannelSuffix", codec)
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.enableSyncWorker()
api.enable()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
@@ -91,29 +90,11 @@ interface BackgroundWorkerFgHostApi {
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val callbackHandleArg = args[0] as Long
val wrapped: List<Any?> = try {
api.enableUploadWorker(callbackHandleArg)
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec)
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.disableUploadWorker()
api.disable()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
@@ -130,6 +111,7 @@ interface BackgroundWorkerFgHostApi {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerBgHostApi {
fun onInitialized()
fun close()
companion object {
/** The codec used by BackgroundWorkerBgHostApi. */
@@ -156,6 +138,22 @@ interface BackgroundWorkerBgHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.close()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -167,23 +165,6 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
BackgroundWorkerPigeonCodec()
}
}
fun onLocalSync(maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(maxSecondsArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName)))
}
}
}
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""

View File

@@ -11,17 +11,11 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor.DartCallback
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.view.FlutterCallbackInformation
private const val TAG = "BackgroundWorker"
enum class BackgroundTaskType {
LOCAL_SYNC,
UPLOAD,
}
class BackgroundWorker(context: Context, params: WorkerParameters) :
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
private val ctx: Context = context.applicationContext
@@ -58,25 +52,6 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
engine = FlutterEngine(ctx)
// Retrieve the callback handle stored by the main Flutter app
// This handle points to the Flutter function that should be executed in the background
val callbackHandle =
ctx.getSharedPreferences(BackgroundWorkerApiImpl.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getLong(BackgroundWorkerApiImpl.SHARED_PREF_CALLBACK_HANDLE, 0L)
if (callbackHandle == 0L) {
// Without a valid callback handle, we cannot start the Flutter background execution
complete(Result.failure())
return@ensureInitializationCompleteAsync
}
// Start the Flutter engine with the specified callback as the entry point
val callback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)
if (callback == null) {
complete(Result.failure())
return@ensureInitializationCompleteAsync
}
// Register custom plugins
MainActivity.registerPlugins(ctx, engine!!)
flutterApi =
@@ -86,8 +61,12 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
api = this
)
engine!!.dartExecutor.executeDartCallback(
DartCallback(ctx.assets, loader.findAppBundlePath(), callback)
engine!!.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
loader.findAppBundlePath(),
"package:immich_mobile/domain/services/background_worker.service.dart",
"backgroundSyncNativeEntrypoint"
)
)
}
@@ -100,23 +79,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* This method acts as a bridge between the native Android background task system and Flutter.
*/
override fun onInitialized() {
val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0)
val taskType = BackgroundTaskType.entries[taskTypeIndex]
when (taskType) {
BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) }
BackgroundTaskType.UPLOAD -> flutterApi?.onAndroidUpload { handleHostResult(it) }
}
flutterApi?.onAndroidUpload { handleHostResult(it) }
}
/**
* Called when the system has to stop this worker because constraints are
* no longer met or the system needs resources for more important tasks
* This is also called when the worker has been explicitly cancelled or replaced
*/
override fun onStopped() {
Log.d(TAG, "About to stop BackupWorker")
override fun close() {
if (isComplete) {
return
}
@@ -134,6 +100,16 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
}, 5000)
}
/**
* Called when the system has to stop this worker because constraints are
* no longer met or the system needs resources for more important tasks
* This is also called when the worker has been explicitly cancelled or replaced
*/
override fun onStopped() {
Log.d(TAG, "About to stop BackupWorker")
close()
}
private fun handleHostResult(result: kotlin.Result<Unit>) {
if (isComplete) {
return
@@ -154,8 +130,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* - Parameter success: Indicates whether the background task completed successfully
*/
private fun complete(success: Result) {
Log.d(TAG, "About to complete BackupWorker with result: $success")
isComplete = true
engine?.destroy()
engine = null
flutterApi = null
completionHandler.set(success)
}

View File

@@ -3,10 +3,8 @@ package app.alextran.immich.background
import android.content.Context
import android.provider.MediaStore
import android.util.Log
import androidx.core.content.edit
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
@@ -16,19 +14,13 @@ private const val TAG = "BackgroundUploadImpl"
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
private val ctx: Context = context.applicationContext
override fun enableSyncWorker() {
override fun enable() {
enqueueMediaObserver(ctx)
Log.i(TAG, "Scheduled media observer")
}
override fun enableUploadWorker(callbackHandle: Long) {
updateUploadEnabled(ctx, true)
updateCallbackHandle(ctx, callbackHandle)
Log.i(TAG, "Scheduled background upload tasks")
}
override fun disableUploadWorker() {
updateUploadEnabled(ctx, false)
override fun disable() {
WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME)
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
Log.i(TAG, "Cancelled background upload tasks")
}
@@ -37,32 +29,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
const val WORKER_DATA_TASK_TYPE = "taskType"
const val SHARED_PREF_NAME = "Immich::Background"
const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled"
const val SHARED_PREF_CALLBACK_HANDLE = "Background::backup::callbackHandle"
private fun updateUploadEnabled(context: Context, enabled: Boolean) {
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled)
}
}
private fun updateCallbackHandle(context: Context, callbackHandle: Long) {
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
putLong(SHARED_PREF_CALLBACK_HANDLE, callbackHandle)
}
}
fun enqueueMediaObserver(ctx: Context) {
val constraints = Constraints.Builder()
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
.setTriggerContentUpdateDelay(5, TimeUnit.SECONDS)
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
.setTriggerContentUpdateDelay(30, TimeUnit.SECONDS)
.setTriggerContentMaxDelay(3, TimeUnit.MINUTES)
.build()
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
@@ -74,15 +48,13 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
}
fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) {
fun enqueueBackgroundWorker(ctx: Context) {
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
val data = Data.Builder()
data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal)
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.setInputData(data.build()).build()
.build()
WorkManager.getInstance(ctx)
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)

View File

@@ -6,29 +6,17 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
private val ctx: Context = context.applicationContext
private val ctx: Context = context.applicationContext
override fun doWork(): Result {
Log.i("MediaObserver", "Content change detected, starting background worker")
override fun doWork(): Result {
Log.i("MediaObserver", "Content change detected, starting background worker")
// Re-enqueue itself to listen for future changes
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
// Enqueue backup worker only if there are new media changes
if (triggeredContentUris.isNotEmpty()) {
val type =
if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type)
}
// Re-enqueue itself to listen for future changes
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
return Result.success()
}
private fun isBackupEnabled(context: Context): Boolean {
val prefs =
context.getSharedPreferences(
BackgroundWorkerApiImpl.SHARED_PREF_NAME,
Context.MODE_PRIVATE
)
return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false)
// Enqueue backup worker only if there are new media changes
if (triggeredContentUris.isNotEmpty()) {
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
}
return Result.success()
}
}

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3011,
"android.injected.version.name" => "1.140.1",
"android.injected.version.code" => 3013,
"android.injected.version.name" => "1.141.1",
}
)
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')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,7 @@ class ImmichTestHelper {
static Future<void> loadApp(WidgetTester tester) async {
await EasyLocalization.ensureInitialized();
// Clear all data from Isar (reuse existing instance if available)
final (isar, drift, logDb) = await Bootstrap.initDB();
final (isar, drift, logDb) = await Bootstrap.initDB(shareAcrossIsolates: true);
await Bootstrap.initDomain(isar, drift, logDb);
await Store.clear();
await isar.writeTxn(() => isar.clear());

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -507,14 +507,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -543,14 +539,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";

View File

@@ -24,7 +24,7 @@ import UIKit
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundWorkerApiImpl.registerBackgroundProcessing()
BackgroundWorkerApiImpl.registerBackgroundWorkers()
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {

View File

@@ -73,9 +73,8 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol BackgroundWorkerFgHostApi {
func enableSyncWorker() throws
func enableUploadWorker(callbackHandle: Int64) throws
func disableUploadWorker() throws
func enable() throws
func disable() throws
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -84,52 +83,38 @@ class BackgroundWorkerFgHostApiSetup {
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
let enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
enableSyncWorkerChannel.setMessageHandler { _, reply in
enableChannel.setMessageHandler { _, reply in
do {
try api.enableSyncWorker()
try api.enable()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
enableSyncWorkerChannel.setMessageHandler(nil)
enableChannel.setMessageHandler(nil)
}
let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
enableUploadWorkerChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let callbackHandleArg = args[0] as! Int64
disableChannel.setMessageHandler { _, reply in
do {
try api.enableUploadWorker(callbackHandle: callbackHandleArg)
try api.disable()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
enableUploadWorkerChannel.setMessageHandler(nil)
}
let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
disableUploadWorkerChannel.setMessageHandler { _, reply in
do {
try api.disableUploadWorker()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
disableUploadWorkerChannel.setMessageHandler(nil)
disableChannel.setMessageHandler(nil)
}
}
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol BackgroundWorkerBgHostApi {
func onInitialized() throws
func close() throws
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -151,11 +136,23 @@ class BackgroundWorkerBgHostApiSetup {
} else {
onInitializedChannel.setMessageHandler(nil)
}
let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
closeChannel.setMessageHandler { _, reply in
do {
try api.close()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
closeChannel.setMessageHandler(nil)
}
}
}
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol BackgroundWorkerFlutterApiProtocol {
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
@@ -170,24 +167,6 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
var codec: BackgroundWorkerPigeonCodec {
return BackgroundWorkerPigeonCodec.shared
}
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage([maxSecondsArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName)))
return
}
if listResponse.count > 1 {
let code: String = listResponse[0] as! String
let message: String? = nilOrValue(listResponse[1])
let details: String? = nilOrValue(listResponse[2])
completion(.failure(PigeonError(code: code, message: message, details: details)))
} else {
completion(.success(()))
}
}
}
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)

View File

@@ -1,7 +1,7 @@
import BackgroundTasks
import Flutter
enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
enum BackgroundTaskType { case refresh, processing }
/*
* DEBUG: Testing Background Tasks in Xcode
@@ -9,10 +9,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
* To test background task functionality during development:
* 1. Pause the application in Xcode debugger
* 2. In the debugger console, enter one of the following commands:
## For local sync (short-running sync):
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
## For background refresh (short-running sync):
@@ -24,8 +20,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
* To simulate task expiration (useful for testing expiration handlers):
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
@@ -86,28 +80,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
* starts the engine, and sets up a timeout timer if specified.
*/
func run() {
// Retrieve the callback handle stored by the main Flutter app
// This handle points to the Flutter function that should be executed in the background
let callbackHandle = Int64(UserDefaults.standard.string(
forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) ?? "0") ?? 0
if callbackHandle == 0 {
// Without a valid callback handle, we cannot start the Flutter background execution
complete(success: false)
return
}
// Use the callback handle to retrieve the actual Flutter callback information
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
// The callback handle is invalid or the callback was not found
complete(success: false)
return
}
// Start the Flutter engine with the specified callback as the entry point
let isRunning = engine.run(
withEntrypoint: callback.callbackName,
libraryURI: callback.callbackLibraryPath
withEntrypoint: "backgroundSyncNativeEntrypoint",
libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart"
)
// Verify that the Flutter engine started successfully
@@ -127,7 +103,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
if maxSeconds != nil {
// Schedule a timer to cancel the task after the specified timeout period
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
self.cancel()
self.close()
}
}
}
@@ -138,25 +114,17 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
* This method acts as a bridge between the native iOS background task system and Flutter.
*/
func onInitialized() throws {
switch self.taskType {
case .refreshUpload, .processingUpload:
flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload,
maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
self.handleHostResult(result: result)
})
case .localSync:
flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
self.handleHostResult(result: result)
})
}
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
self.handleHostResult(result: result)
})
}
/**
* Cancels the currently running background task, either due to timeout or external request.
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
* the completion handler is eventually called even if Flutter doesn't respond.
*/
func cancel() {
func close() {
if isComplete {
return
}
@@ -172,6 +140,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
self.complete(success: false)
}
}
/**
* Handles the result from Flutter API calls and determines the success/failure status.
@@ -182,7 +151,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
private func handleHostResult(result: Result<Void, PigeonError>) {
switch result {
case .success(): self.complete(success: true)
case .failure(_): self.cancel()
case .failure(_): self.close()
}
}
@@ -195,6 +164,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
* - Parameter success: Indicates whether the background task completed successfully
*/
private func complete(success: Bool) {
if(isComplete) {
return
}
isComplete = true
engine.destroyContext()
completionHandler(success)

View File

@@ -1,84 +1,40 @@
import BackgroundTasks
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
func enableSyncWorker() throws {
BackgroundWorkerApiImpl.scheduleLocalSync()
print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled")
func enable() throws {
BackgroundWorkerApiImpl.scheduleRefreshWorker()
BackgroundWorkerApiImpl.scheduleProcessingWorker()
print("BackgroundUploadImpl:enbale Background worker scheduled")
}
func enableUploadWorker(callbackHandle: Int64) throws {
BackgroundWorkerApiImpl.updateUploadEnabled(true)
// Store the callback handle for later use when starting background Flutter isolates
BackgroundWorkerApiImpl.updateUploadCallbackHandle(callbackHandle)
BackgroundWorkerApiImpl.scheduleRefreshUpload()
BackgroundWorkerApiImpl.scheduleProcessingUpload()
print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks")
func disable() throws {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
print("BackgroundUploadImpl:disableUploadWorker Disabled background workers")
}
func disableUploadWorker() throws {
BackgroundWorkerApiImpl.updateUploadEnabled(false)
BackgroundWorkerApiImpl.cancelUploadTasks()
print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks")
}
public static let backgroundUploadEnabledKey = "immich:background:backup:enabled"
public static let backgroundUploadCallbackHandleKey = "immich:background:backup:callbackHandle"
private static let localSyncTaskID = "app.alextran.immich.background.localSync"
private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload"
private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload"
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
private static func updateUploadEnabled(_ isEnabled: Bool) {
return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey)
}
private static func updateUploadCallbackHandle(_ callbackHandle: Int64) {
return UserDefaults.standard.set(String(callbackHandle), forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey)
}
private static func cancelUploadTasks() {
BackgroundWorkerApiImpl.updateUploadEnabled(false)
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID);
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID);
}
public static func registerBackgroundProcessing() {
public static func registerBackgroundWorkers() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in
forTaskWithIdentifier: processingTaskID, using: nil) { task in
if task is BGProcessingTask {
handleBackgroundProcessing(task: task as! BGProcessingTask)
}
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
if task is BGAppRefreshTask {
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload)
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
}
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: localSyncTaskID, using: nil) { task in
if task is BGAppRefreshTask {
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync)
}
}
}
private static func scheduleLocalSync() {
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID)
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
do {
try BGTaskScheduler.shared.submit(backgroundRefresh)
} catch {
print("Could not schedule the local sync task \(error.localizedDescription)")
}
}
private static func scheduleRefreshUpload() {
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID)
private static func scheduleRefreshWorker() {
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID)
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
do {
@@ -88,8 +44,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
}
}
private static func scheduleProcessingUpload() {
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID)
private static func scheduleProcessingWorker() {
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID)
backgroundProcessing.requiresNetworkConnectivity = true
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
@@ -101,16 +57,16 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
}
}
private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) {
scheduleRefreshUpload()
// Restrict the refresh task to run only for a maximum of 20 seconds
runBackgroundWorker(task: task, taskType: taskType, maxSeconds: 20)
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
scheduleRefreshWorker()
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
}
private static func handleBackgroundProcessing(task: BGProcessingTask) {
scheduleProcessingUpload()
scheduleProcessingWorker()
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil)
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
}
/**
@@ -134,7 +90,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
task.expirationHandler = {
DispatchQueue.main.async {
backgroundWorker.cancel()
backgroundWorker.close()
}
isSuccess = false

View File

@@ -46,6 +46,23 @@ class ThumbnailApiImpl: ThumbnailApi {
assetCache.countLimit = 10000
return assetCache
}()
private static let activitySemaphore = DispatchSemaphore(value: 1)
private static let willResignActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willResignActiveNotification,
object: nil,
queue: .main
) { _ in
processingQueue.suspend()
activitySemaphore.wait()
}
private static let didBecomeActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
processingQueue.resume()
activitySemaphore.signal()
}
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
Self.processingQueue.async {
@@ -53,6 +70,7 @@ class ThumbnailApiImpl: ThumbnailApi {
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
let (width, height, pointer) = thumbHashToRGBA(hash: data)
self.waitForActiveState()
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
}
}
@@ -142,6 +160,7 @@ class ThumbnailApiImpl: ThumbnailApi {
return completion(Self.cancelledResult)
}
self.waitForActiveState()
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
Self.removeRequest(requestId: requestId)
}
@@ -184,4 +203,9 @@ class ThumbnailApiImpl: ThumbnailApi {
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
return asset
}
func waitForActiveState() {
Self.activitySemaphore.wait()
Self.activitySemaphore.signal()
}
}

View File

@@ -1,190 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.alextran.immich.background.localSync</string>
<string>app.alextran.immich.background.refreshUpload</string>
<string>app.alextran.immich.background.processingUpload</string>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>ShareHandler</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.file-url</string>
<string>public.image</string>
<string>public.text</string>
<string>public.movie</string>
<string>public.url</string>
<string>public.data</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
<string>ca</string>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>he</string>
<string>hi</string>
<string>hu</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>lv</string>
<string>mn</string>
<string>nb</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ro</string>
<string>ru</string>
<string>sk</string>
<string>sl</string>
<string>sr</string>
<string>sv</string>
<string>th</string>
<string>uk</string>
<string>vi</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.140.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>Share Extension</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>Deep Link</string>
<key>CFBundleURLSchemes</key>
<array>
<string>immich</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>219</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
<false />
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true />
<key>LSSupportsOpeningDocumentsInPlace</key>
<string>No</string>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSFaceIDUsageDescription</key>
<string>We need to use FaceID to allow access to your locked folder</string>
<key>NSLocalNetworkUsageDescription</key>
<string>We need local network permission to connect to the local server using IP address and
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.alextran.immich.background.refreshUpload</string>
<string>app.alextran.immich.background.processingUpload</string>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>ShareHandler</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.file-url</string>
<string>public.image</string>
<string>public.text</string>
<string>public.movie</string>
<string>public.url</string>
<string>public.data</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
<string>ca</string>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>he</string>
<string>hi</string>
<string>hu</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>lv</string>
<string>mn</string>
<string>nb</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ro</string>
<string>ru</string>
<string>sk</string>
<string>sl</string>
<string>sr</string>
<string>sv</string>
<string>th</string>
<string>uk</string>
<string>vi</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.140.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>Share Extension</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>Deep Link</string>
<key>CFBundleURLSchemes</key>
<array>
<string>immich</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>219</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<string>No</string>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSFaceIDUsageDescription</key>
<string>We need to use FaceID to allow access to your locked folder</string>
<key>NSLocalNetworkUsageDescription</key>
<string>We need local network permission to connect to the local server using IP address and
allow the casting feature to work</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
<key>NSLocationUsageDescription</key>
<string>We require this permission to access the local WiFi name</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We require this permission to access the local WiFi name</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false />
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true />
<key>io.flutter.embedded_views_preview</key>
<true />
</dict>
</plist>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
<key>NSLocationUsageDescription</key>
<string>We require this permission to access the local WiFi name</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We require this permission to access the local WiFi name</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
</dict>
</plist>

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.140.1"
version_number: "1.141.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -15,6 +15,7 @@ class LocalAlbum {
final int assetCount;
final BackupSelection backupSelection;
final String? linkedRemoteAlbumId;
const LocalAlbum({
required this.id,
@@ -23,6 +24,7 @@ class LocalAlbum {
this.assetCount = 0,
this.backupSelection = BackupSelection.none,
this.isIosSharedAlbum = false,
this.linkedRemoteAlbumId,
});
LocalAlbum copyWith({
@@ -32,6 +34,7 @@ class LocalAlbum {
int? assetCount,
BackupSelection? backupSelection,
bool? isIosSharedAlbum,
String? linkedRemoteAlbumId,
}) {
return LocalAlbum(
id: id ?? this.id,
@@ -40,6 +43,7 @@ class LocalAlbum {
assetCount: assetCount ?? this.assetCount,
backupSelection: backupSelection ?? this.backupSelection,
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
);
}
@@ -53,7 +57,8 @@ class LocalAlbum {
other.updatedAt == updatedAt &&
other.assetCount == assetCount &&
other.backupSelection == backupSelection &&
other.isIosSharedAlbum == isIosSharedAlbum;
other.isIosSharedAlbum == isIosSharedAlbum &&
other.linkedRemoteAlbumId == linkedRemoteAlbumId;
}
@override
@@ -63,7 +68,8 @@ class LocalAlbum {
updatedAt.hashCode ^
assetCount.hashCode ^
backupSelection.hashCode ^
isIosSharedAlbum.hashCode;
isIosSharedAlbum.hashCode ^
linkedRemoteAlbumId.hashCode;
}
@override
@@ -75,6 +81,7 @@ updatedAt: $updatedAt,
assetCount: $assetCount,
backupSelection: $backupSelection,
isIosSharedAlbum: $isIosSharedAlbum
linkedRemoteAlbumId: $linkedRemoteAlbumId,
}''';
}
}

View File

@@ -1,7 +1,36 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'dart:ui';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary("primary"),
pink("pink"),
red("red"),
yellow("yellow"),
blue("blue"),
green("green"),
purple("purple"),
orange("orange"),
gray("gray"),
amber("amber");
final String value;
const AvatarColor(this.value);
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
// TODO: Rename to User once Isar is removed
class UserDto {
@@ -9,7 +38,7 @@ class UserDto {
final String email;
final String name;
final bool isAdmin;
final DateTime updatedAt;
final DateTime? updatedAt;
final AvatarColor avatarColor;
@@ -31,8 +60,8 @@ class UserDto {
required this.id,
required this.email,
required this.name,
required this.isAdmin,
required this.updatedAt,
this.isAdmin = false,
this.updatedAt,
required this.profileChangedAt,
this.avatarColor = AvatarColor.primary,
this.memoryEnabled = true,
@@ -99,7 +128,8 @@ profileChangedAt: $profileChangedAt
if (identical(this, other)) return true;
return other.id == id &&
other.updatedAt.isAtSameMomentAs(updatedAt) &&
((updatedAt == null && other.updatedAt == null) ||
(updatedAt != null && other.updatedAt != null && other.updatedAt!.isAtSameMomentAs(updatedAt!))) &&
other.avatarColor == avatarColor &&
other.email == email &&
other.name == name &&

View File

@@ -1,4 +1,4 @@
import 'dart:ui';
import 'package:immich_mobile/domain/models/user.model.dart';
enum UserMetadataKey {
// do not change this order!
@@ -7,36 +7,6 @@ enum UserMetadataKey {
license,
}
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary("primary"),
pink("pink"),
red("red"),
yellow("yellow"),
blue("blue"),
green("green"),
purple("purple"),
orange("orange"),
gray("gray"),
amber("amber");
final String value;
const AvatarColor(this.value);
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary => isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
class Onboarding {
final bool isOnboarded;

View File

@@ -5,6 +5,7 @@ import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
@@ -14,6 +15,7 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
@@ -29,13 +31,9 @@ class BackgroundWorkerFgService {
const BackgroundWorkerFgService(this._foregroundHostApi);
// TODO: Move this call to native side once old timeline is removed
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
Future<void> enable() => _foregroundHostApi.enable();
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker(
PluginUtilities.getCallbackHandle(_backgroundSyncNativeEntrypoint)!.toRawHandle(),
);
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
Future<void> disable() => _foregroundHostApi.disable();
}
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
@@ -45,6 +43,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi;
final Logger _logger = Logger('BackgroundUploadBgService');
late final IsolateLockManager _lockManager;
bool _isCleanedUp = false;
@@ -60,98 +59,106 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
driftProvider.overrideWith(driftOverride(drift)),
],
);
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
BackgroundWorkerFlutterApi.setUp(this);
}
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
Future<void> init() async {
await loadTranslations();
HttpSSLOptions.apply(applyNative: false);
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
try {
await loadTranslations();
HttpSSLOptions.apply(applyNative: false);
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
// Initialize the file downloader
await FileDownloader().configure(
globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256),
],
);
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
await FileDownloader().trackTasks();
configureFileDownloaderNotifications();
// Initialize the file downloader
await FileDownloader().configure(
globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256),
],
);
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
await FileDownloader().trackTasks();
configureFileDownloaderNotifications();
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
// Notify the host that the background upload service has been initialized and is ready to use
await _backgroundHostApi.onInitialized();
// Notify the host that the background upload service has been initialized and is ready to use
debugPrint("Acquiring background worker lock");
if (await _lockManager.acquireLock().timeout(
const Duration(seconds: 5),
onTimeout: () {
_lockManager.cancel();
return false;
},
)) {
_logger.info("Acquired background worker lock");
await _backgroundHostApi.onInitialized();
return;
}
_logger.warning("Failed to acquire background worker lock");
await _cleanup();
await _backgroundHostApi.close();
} catch (error, stack) {
_logger.severe("Failed to initialize background worker", error, stack);
_backgroundHostApi.close();
}
}
@override
Future<void> onLocalSync(int? maxSeconds) async {
_logger.info('Local background syncing started');
final sw = Stopwatch()..start();
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
await _syncAssets(hashTimeout: timeout, syncRemote: false);
sw.stop();
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s");
}
/* We do the following on Android upload
* - Sync local assets
* - Hash local assets 3 / 6 minutes
* - Sync remote assets
* - Check and requeue upload tasks
*/
@override
Future<void> onAndroidUpload() async {
_logger.info('Android background processing started');
final sw = Stopwatch()..start();
try {
_logger.info('Android background processing started');
final sw = Stopwatch()..start();
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
await _handleBackup(processBulk: false);
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
await _handleBackup(processBulk: false);
await _cleanup();
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
} catch (error, stack) {
_logger.severe("Failed to complete Android background processing", error, stack);
} finally {
await _cleanup();
}
}
/* We do the following on background upload
* - Sync local assets
* - Hash local assets
* - Sync remote assets
* - Check and requeue upload tasks
*
* The native side will not send the maxSeconds value for processing tasks
*/
@override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
final sw = Stopwatch()..start();
try {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
final sw = Stopwatch()..start();
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
await _syncAssets(hashTimeout: timeout);
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
await _syncAssets(hashTimeout: timeout);
final backupFuture = _handleBackup();
if (maxSeconds != null) {
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
} else {
await backupFuture;
final backupFuture = _handleBackup();
if (maxSeconds != null) {
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
} else {
await backupFuture;
}
sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
} catch (error, stack) {
_logger.severe("Failed to complete iOS background upload", error, stack);
} finally {
await _cleanup();
}
await _cleanup();
sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
}
@override
Future<void> cancel() async {
_logger.warning("Background upload cancelled");
await _cleanup();
_logger.warning("Background worker cancelled");
try {
await _cleanup();
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
}
}
Future<void> _cleanup() async {
@@ -159,13 +166,27 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
return;
}
_isCleanedUp = true;
await _ref.read(backgroundSyncProvider).cancel();
await _ref.read(backgroundSyncProvider).cancelLocal();
await _isar.close();
await _drift.close();
await _driftLogger.close();
_ref.dispose();
try {
_isCleanedUp = true;
_logger.info("Cleaning up background worker");
final cleanupFutures = [
_drift.close(),
_driftLogger.close(),
_ref.read(backgroundSyncProvider).cancel(),
_ref.read(backgroundSyncProvider).cancelLocal(),
];
if (_isar.isOpen) {
cleanupFutures.add(_isar.close());
}
_ref.dispose();
_lockManager.releaseLock();
await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up");
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
}
}
Future<void> _handleBackup({bool processBulk = true}) async {
@@ -190,7 +211,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
}
Future<void> _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async {
Future<void> _syncAssets({Duration? hashTimeout}) async {
final futures = <Future<void>>[];
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
@@ -212,21 +233,20 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
});
futures.add(localSyncFuture);
if (syncRemote) {
final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote();
futures.add(remoteSyncFuture);
}
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
await Future.wait(futures);
}
}
/// Native entry invoked from the background worker. If renaming or moving this to a different
/// library, make sure to update the entry points and URI in native workers as well
@pragma('vm:entry-point')
Future<void> _backgroundSyncNativeEntrypoint() async {
Future<void> backgroundSyncNativeEntrypoint() async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDB) = await Bootstrap.initDB();
final (isar, drift, logDB) = await Bootstrap.initDB(shareAcrossIsolates: false);
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false);
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
}

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
@@ -35,6 +36,7 @@ class HashService {
bool get isCancelled => _cancelChecker?.call() ?? false;
Future<void> hashAssets() async {
_log.info("Starting hashing of assets");
final Stopwatch stopwatch = Stopwatch()..start();
// Sorted by backupSelection followed by isCloud
final localAlbums = await _localAlbumRepository.getAll(
@@ -49,7 +51,7 @@ class HashService {
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
if (assetsToHash.isNotEmpty) {
await _hashAssets(assetsToHash);
await _hashAssets(album, assetsToHash);
}
}
@@ -60,7 +62,7 @@ class HashService {
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
/// with hash for those that were successfully hashed. Hashes are looked up in a table
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
Future<void> _hashAssets(List<LocalAsset> assetsToHash) async {
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
int bytesProcessed = 0;
final toHash = <_AssetToPath>[];
@@ -72,6 +74,9 @@ class HashService {
final file = await _storageRepository.getFileForAsset(asset.id);
if (file == null) {
_log.warning(
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt} from album: ${album.name}",
);
continue;
}
@@ -79,17 +84,17 @@ class HashService {
toHash.add(_AssetToPath(asset: asset, path: file.path));
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
await _processBatch(toHash);
await _processBatch(album, toHash);
toHash.clear();
bytesProcessed = 0;
}
}
await _processBatch(toHash);
await _processBatch(album, toHash);
}
/// Processes a batch of assets.
Future<void> _processBatch(List<_AssetToPath> toHash) async {
Future<void> _processBatch(LocalAlbum album, List<_AssetToPath> toHash) async {
if (toHash.isEmpty) {
return;
}
@@ -114,7 +119,9 @@ class HashService {
if (hash?.length == 20) {
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
} else {
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
_log.warning(
"Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}",
);
}
}

View File

@@ -22,4 +22,16 @@ class LocalAlbumService {
Future<int> getCount() {
return _repository.getCount();
}
Future<void> unlinkRemoteAlbum(String id) async {
return _repository.unlinkRemoteAlbum(id);
}
Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
return _repository.linkRemoteAlbum(localAlbumId, remoteAlbumId);
}
Future<List<LocalAlbum>> getBackupAlbums() {
return _repository.getBackupAlbums();
}
}

View File

@@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
import 'package:platform/platform.dart';
@@ -285,7 +286,7 @@ extension on Iterable<PlatformAlbum> {
(e) => LocalAlbum(
id: e.id,
name: e.name,
updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
assetCount: e.assetCount,
),
).toList();
@@ -300,8 +301,8 @@ extension on Iterable<PlatformAsset> {
name: e.name,
checksum: null,
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
createdAt: e.createdAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000),
updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
createdAt: tryFromSecondsSinceEpoch(e.createdAt) ?? DateTime.now(),
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
width: e.width,
height: e.height,
durationInSeconds: e.durationInSeconds,

View File

@@ -26,6 +26,10 @@ class RemoteAlbumService {
return _repository.get(albumId);
}
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
return _repository.getByName(albumName, ownerId);
}
Future<List<RemoteAlbum>> sortAlbums(
List<RemoteAlbum> albums,
RemoteAlbumSortMode sortMode, {
@@ -80,7 +84,6 @@ class RemoteAlbumService {
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
await _repository.create(album, assetIds);
return album;

View File

@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService(
ref.watch(localAlbumRepository),
ref.watch(remoteAlbumRepository),
ref.watch(driftAlbumApiRepositoryProvider),
),
);
class SyncLinkedAlbumService {
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository;
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
Future<void> syncLinkedAlbums(String userId) async {
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
await Future.wait(
selectedAlbums.map((localAlbum) async {
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
if (linkedRemoteAlbumId == null) {
return;
}
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
if (remoteAlbum == null) {
return;
}
// get assets that are uploaded but not in the remote album
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
if (assetIds.isNotEmpty) {
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
}
}),
);
}
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
for (final album in localAlbums) {
await _processLocalAlbum(album, ownerId);
}
}
/// Processes a single local album to ensure proper linking with remote albums
Future<void> _processLocalAlbum(LocalAlbum localAlbum, String ownerId) {
final hasLinkedRemoteAlbum = localAlbum.linkedRemoteAlbumId != null;
if (hasLinkedRemoteAlbum) {
return _handleLinkedAlbum(localAlbum);
} else {
return _handleUnlinkedAlbum(localAlbum, ownerId);
}
}
/// Handles albums that are already linked to a remote album
Future<void> _handleLinkedAlbum(LocalAlbum localAlbum) async {
final remoteAlbumId = localAlbum.linkedRemoteAlbumId!;
final remoteAlbum = await _remoteAlbumRepository.get(remoteAlbumId);
final remoteAlbumExists = remoteAlbum != null;
if (!remoteAlbumExists) {
return _localAlbumRepository.unlinkRemoteAlbum(localAlbum.id);
}
}
/// Handles albums that are not linked to any remote album
Future<void> _handleUnlinkedAlbum(LocalAlbum localAlbum, String ownerId) async {
final existingRemoteAlbum = await _remoteAlbumRepository.getByName(localAlbum.name, ownerId);
if (existingRemoteAlbum != null) {
return _linkToExistingRemoteAlbum(localAlbum, existingRemoteAlbum);
} else {
return _createAndLinkNewRemoteAlbum(localAlbum);
}
}
/// Links a local album to an existing remote album
Future<void> _linkToExistingRemoteAlbum(LocalAlbum localAlbum, dynamic existingRemoteAlbum) {
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, existingRemoteAlbum.id);
}
/// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
}
}

View File

@@ -23,54 +23,17 @@ class SyncStreamService {
bool get isCancelled => _cancelChecker?.call() ?? false;
Future<void> sync() {
Future<void> sync() async {
_logger.info("Remote sync request for user");
// Start the sync stream and handle events
return _syncApiRepository.streamChanges(_handleEvents);
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
final List<SyncAssetV1> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV1.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
bool shouldReset = false;
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
if (shouldReset) {
await _syncApiRepository.streamChanges(_handleEvents);
}
}
Future<void> _handleEvents(List<SyncEvent> events, Function() abort) async {
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
List<SyncEvent> items = [];
for (final event in events) {
if (isCancelled) {
@@ -83,6 +46,10 @@ class SyncStreamService {
await _processBatch(items);
}
if (event.type == SyncEntityType.syncResetV1) {
reset();
}
items.add(event);
}
@@ -103,6 +70,8 @@ class SyncStreamService {
Future<void> _handleSyncData(SyncEntityType type, Iterable<Object> data) async {
_logger.fine("Processing sync data for $type of length ${data.length}");
switch (type) {
case SyncEntityType.authUserV1:
return _syncStreamRepository.updateAuthUsersV1(data.cast());
case SyncEntityType.userV1:
return _syncStreamRepository.updateUsersV1(data.cast());
case SyncEntityType.userDeleteV1:
@@ -159,6 +128,12 @@ class SyncStreamService {
// to acknowledge that the client has processed all the backfill events
case SyncEntityType.syncAckV1:
return;
// No-op. SyncCompleteV1 is used to signal the completion of the sync process
case SyncEntityType.syncCompleteV1:
return;
// Request to reset the client state. Clear everything related to remote entities
case SyncEntityType.syncResetV1:
return _syncStreamRepository.reset();
case SyncEntityType.memoryV1:
return _syncStreamRepository.updateMemoriesV1(data.cast());
case SyncEntityType.memoryDeleteV1:
@@ -193,4 +168,45 @@ class SyncStreamService {
_logger.warning("Unknown sync data type: $type");
}
}
Future<void> handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
final List<SyncAssetV1> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV1.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/utils/isolate.dart';
import 'package:worker_manager/worker_manager.dart';
@@ -23,6 +24,7 @@ class BackgroundSyncManager {
Cancelable<void>? _syncTask;
Cancelable<void>? _syncWebsocketTask;
Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _linkedAlbumSyncTask;
Cancelable<void>? _hashTask;
BackgroundSyncManager({
@@ -52,6 +54,12 @@ class BackgroundSyncManager {
_syncWebsocketTask?.cancel();
_syncWebsocketTask = null;
if (_linkedAlbumSyncTask != null) {
futures.add(_linkedAlbumSyncTask!.future);
}
_linkedAlbumSyncTask?.cancel();
_linkedAlbumSyncTask = null;
try {
await Future.wait(futures);
} on CanceledError {
@@ -155,6 +163,17 @@ class BackgroundSyncManager {
_syncWebsocketTask = null;
});
}
Future<void> syncLinkedAlbum() {
if (_linkedAlbumSyncTask != null) {
return _linkedAlbumSyncTask!.future;
}
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
return _linkedAlbumSyncTask!.whenComplete(() {
_linkedAlbumSyncTask = null;
});
}
}
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(

View File

@@ -0,0 +1,235 @@
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
const String kIsolateLockManagerPort = "immich://isolate_mutex";
enum _LockStatus { active, released }
class _IsolateRequest {
const _IsolateRequest();
}
class _HeartbeatRequest extends _IsolateRequest {
// Port for the receiver to send replies back
final SendPort sendPort;
const _HeartbeatRequest(this.sendPort);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'sendPort': sendPort};
}
}
class _CloseRequest extends _IsolateRequest {
const _CloseRequest();
Map<String, dynamic> toJson() {
return {'type': 'close'};
}
}
class _IsolateResponse {
const _IsolateResponse();
}
class _HeartbeatResponse extends _IsolateResponse {
final _LockStatus status;
const _HeartbeatResponse(this.status);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'status': status.index};
}
}
typedef OnCloseLockHolderRequest = void Function();
class IsolateLockManager {
final String _portName;
bool _hasLock = false;
ReceivePort? _receivePort;
final OnCloseLockHolderRequest? _onCloseRequest;
final Set<SendPort> _waitingIsolates = {};
// Token object - a new one is created for each acquisition attempt
Object? _currentAcquisitionToken;
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
: _portName = portName ?? kIsolateLockManagerPort,
_onCloseRequest = onCloseRequest;
Future<bool> acquireLock() async {
if (_hasLock) {
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
return true;
}
// Create a new token - this invalidates any previous attempt
final token = _currentAcquisitionToken = Object();
final ReceivePort rp = _receivePort = ReceivePort(_portName);
final SendPort sp = rp.sendPort;
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
return false;
}
await _lockReleasedByHolder(token);
}
_hasLock = true;
rp.listen(_onRequest);
return true;
}
Future<void> _lockReleasedByHolder(Object token) async {
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
debugPrint("Found lock holder: $holder");
if (holder == null) {
// No holder, try and acquire lock
return;
}
final ReceivePort tempRp = ReceivePort();
final SendPort tempSp = tempRp.sendPort;
final bs = tempRp.asBroadcastStream();
try {
while (true) {
// Send a heartbeat request with the send port to receive reply from the holder
debugPrint("Sending heartbeat request to lock holder");
holder.send(_HeartbeatRequest(tempSp).toJson());
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
debugPrint("Received heartbeat response from lock holder: $answer");
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
break;
}
if (answer == null) {
// Holder failed, most likely killed without calling releaseLock
// Check if a different waiting isolate took the lock
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
// No, remove the stale lock
IsolateNameServer.removePortNameMapping(_portName);
}
break;
}
// Unknown message type received for heartbeat request. Try again
_IsolateResponse? response = _parseResponse(answer);
if (response == null || response is! _HeartbeatResponse) {
break;
}
if (response.status == _LockStatus.released) {
// Holder has released the lock
break;
}
// If the _LockStatus is active, we check again if the task completed
// by sending a released messaged again, if not, send a new heartbeat again
// Check if the holder completed its task after the heartbeat
answer = await bs.first.timeout(
const Duration(seconds: 3),
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
);
response = _parseResponse(answer);
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
break;
}
}
} catch (e) {
// Timeout or error
} finally {
tempRp.close();
}
return;
}
_IsolateRequest? _parseRequest(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
'close' => const _CloseRequest(),
_ => null,
};
}
_IsolateResponse? _parseResponse(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
_ => null,
};
}
// Executed in the isolate with the lock
void _onRequest(dynamic msg) {
final request = _parseRequest(msg);
if (request == null) {
return;
}
if (request is _HeartbeatRequest) {
// Add the send port to the list of waiting isolates
_waitingIsolates.add(request.sendPort);
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
return;
}
if (request is _CloseRequest) {
_onCloseRequest?.call();
return;
}
}
void releaseLock() {
if (_hasLock) {
IsolateNameServer.removePortNameMapping(_portName);
// Notify waiting isolates
for (final port in _waitingIsolates) {
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
}
_waitingIsolates.clear();
_hasLock = false;
}
_receivePort?.close();
_receivePort = null;
}
void cancel() {
if (_hasLock) {
return;
}
debugPrint("Cancelling ongoing acquire lock attempts");
// Create a new token to invalidate ongoing acquire lock attempts
_currentAcquisitionToken = Object();
}
void requestHolderToClose() {
if (_hasLock) {
return;
}
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
}
}

View File

@@ -0,0 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/providers/user.provider.dart';
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
final user = ref.read(currentUserProvider);
if (user == null) {
return Future.value();
}
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
}

View File

@@ -0,0 +1,27 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class AuthUserEntity extends Table with DriftDefaultsMixin {
const AuthUserEntity();
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get email => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
// Profile image
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get avatarColor => intEnum<AvatarColor>()();
// Quota
IntColumn get quotaSizeInBytes => integer().withDefault(const Constant(0))();
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
// Locked Folder
TextColumn get pinCode => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}

View File

@@ -0,0 +1,933 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/user.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$AuthUserEntityTableCreateCompanionBuilder =
i1.AuthUserEntityCompanion Function({
required String id,
required String name,
required String email,
i0.Value<bool> isAdmin,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
required i2.AvatarColor avatarColor,
i0.Value<int> quotaSizeInBytes,
i0.Value<int> quotaUsageInBytes,
i0.Value<String?> pinCode,
});
typedef $$AuthUserEntityTableUpdateCompanionBuilder =
i1.AuthUserEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> name,
i0.Value<String> email,
i0.Value<bool> isAdmin,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
i0.Value<i2.AvatarColor> avatarColor,
i0.Value<int> quotaSizeInBytes,
i0.Value<int> quotaUsageInBytes,
i0.Value<String?> pinCode,
});
class $$AuthUserEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
$$AuthUserEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get hasProfileImage => $composableBuilder(
column: $table.hasProfileImage,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get profileChangedAt => $composableBuilder(
column: $table.profileChangedAt,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<i2.AvatarColor, i2.AvatarColor, int>
get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get pinCode => $composableBuilder(
column: $table.pinCode,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$AuthUserEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
$$AuthUserEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get name => $composableBuilder(
column: $table.name,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get hasProfileImage => $composableBuilder(
column: $table.hasProfileImage,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get profileChangedAt => $composableBuilder(
column: $table.profileChangedAt,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get pinCode => $composableBuilder(
column: $table.pinCode,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$AuthUserEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AuthUserEntityTable> {
$$AuthUserEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumn<String> get email =>
$composableBuilder(column: $table.email, builder: (column) => column);
i0.GeneratedColumn<bool> get isAdmin =>
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
i0.GeneratedColumn<bool> get hasProfileImage => $composableBuilder(
column: $table.hasProfileImage,
builder: (column) => column,
);
i0.GeneratedColumn<DateTime> get profileChangedAt => $composableBuilder(
column: $table.profileChangedAt,
builder: (column) => column,
);
i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int> get avatarColor =>
$composableBuilder(
column: $table.avatarColor,
builder: (column) => column,
);
i0.GeneratedColumn<int> get quotaSizeInBytes => $composableBuilder(
column: $table.quotaSizeInBytes,
builder: (column) => column,
);
i0.GeneratedColumn<int> get quotaUsageInBytes => $composableBuilder(
column: $table.quotaUsageInBytes,
builder: (column) => column,
);
i0.GeneratedColumn<String> get pinCode =>
$composableBuilder(column: $table.pinCode, builder: (column) => column);
}
class $$AuthUserEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData,
i1.$$AuthUserEntityTableFilterComposer,
i1.$$AuthUserEntityTableOrderingComposer,
i1.$$AuthUserEntityTableAnnotationComposer,
$$AuthUserEntityTableCreateCompanionBuilder,
$$AuthUserEntityTableUpdateCompanionBuilder,
(
i1.AuthUserEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData
>,
),
i1.AuthUserEntityData,
i0.PrefetchHooks Function()
> {
$$AuthUserEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$AuthUserEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$AuthUserEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$AuthUserEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$AuthUserEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
i0.Value<int> quotaSizeInBytes = const i0.Value.absent(),
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
i0.Value<String?> pinCode = const i0.Value.absent(),
}) => i1.AuthUserEntityCompanion(
id: id,
name: name,
email: email,
isAdmin: isAdmin,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
avatarColor: avatarColor,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
pinCode: pinCode,
),
createCompanionCallback:
({
required String id,
required String name,
required String email,
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
required i2.AvatarColor avatarColor,
i0.Value<int> quotaSizeInBytes = const i0.Value.absent(),
i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
i0.Value<String?> pinCode = const i0.Value.absent(),
}) => i1.AuthUserEntityCompanion.insert(
id: id,
name: name,
email: email,
isAdmin: isAdmin,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
avatarColor: avatarColor,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
pinCode: pinCode,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$AuthUserEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData,
i1.$$AuthUserEntityTableFilterComposer,
i1.$$AuthUserEntityTableOrderingComposer,
i1.$$AuthUserEntityTableAnnotationComposer,
$$AuthUserEntityTableCreateCompanionBuilder,
$$AuthUserEntityTableUpdateCompanionBuilder,
(
i1.AuthUserEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AuthUserEntityTable,
i1.AuthUserEntityData
>,
),
i1.AuthUserEntityData,
i0.PrefetchHooks Function()
>;
class $AuthUserEntityTable extends i3.AuthUserEntity
with i0.TableInfo<$AuthUserEntityTable, i1.AuthUserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$AuthUserEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _nameMeta = const i0.VerificationMeta(
'name',
);
@override
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
'name',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta(
'email',
);
@override
late final i0.GeneratedColumn<String> email = i0.GeneratedColumn<String>(
'email',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta(
'isAdmin',
);
@override
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
'is_admin',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_admin" IN (0, 1))',
),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _hasProfileImageMeta =
const i0.VerificationMeta('hasProfileImage');
@override
late final i0.GeneratedColumn<bool> hasProfileImage =
i0.GeneratedColumn<bool>(
'has_profile_image',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("has_profile_image" IN (0, 1))',
),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _profileChangedAtMeta =
const i0.VerificationMeta('profileChangedAt');
@override
late final i0.GeneratedColumn<DateTime> profileChangedAt =
i0.GeneratedColumn<DateTime>(
'profile_changed_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime,
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int>
avatarColor =
i0.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: true,
).withConverter<i2.AvatarColor>(
i1.$AuthUserEntityTable.$converteravatarColor,
);
static const i0.VerificationMeta _quotaSizeInBytesMeta =
const i0.VerificationMeta('quotaSizeInBytes');
@override
late final i0.GeneratedColumn<int> quotaSizeInBytes = i0.GeneratedColumn<int>(
'quota_size_in_bytes',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
);
static const i0.VerificationMeta _quotaUsageInBytesMeta =
const i0.VerificationMeta('quotaUsageInBytes');
@override
late final i0.GeneratedColumn<int> quotaUsageInBytes =
i0.GeneratedColumn<int>(
'quota_usage_in_bytes',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
);
static const i0.VerificationMeta _pinCodeMeta = const i0.VerificationMeta(
'pinCode',
);
@override
late final i0.GeneratedColumn<String> pinCode = i0.GeneratedColumn<String>(
'pin_code',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
@override
List<i0.GeneratedColumn> get $columns => [
id,
name,
email,
isAdmin,
hasProfileImage,
profileChangedAt,
avatarColor,
quotaSizeInBytes,
quotaUsageInBytes,
pinCode,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'auth_user_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.AuthUserEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('name')) {
context.handle(
_nameMeta,
name.isAcceptableOrUnknown(data['name']!, _nameMeta),
);
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('email')) {
context.handle(
_emailMeta,
email.isAcceptableOrUnknown(data['email']!, _emailMeta),
);
} else if (isInserting) {
context.missing(_emailMeta);
}
if (data.containsKey('is_admin')) {
context.handle(
_isAdminMeta,
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta),
);
}
if (data.containsKey('has_profile_image')) {
context.handle(
_hasProfileImageMeta,
hasProfileImage.isAcceptableOrUnknown(
data['has_profile_image']!,
_hasProfileImageMeta,
),
);
}
if (data.containsKey('profile_changed_at')) {
context.handle(
_profileChangedAtMeta,
profileChangedAt.isAcceptableOrUnknown(
data['profile_changed_at']!,
_profileChangedAtMeta,
),
);
}
if (data.containsKey('quota_size_in_bytes')) {
context.handle(
_quotaSizeInBytesMeta,
quotaSizeInBytes.isAcceptableOrUnknown(
data['quota_size_in_bytes']!,
_quotaSizeInBytesMeta,
),
);
}
if (data.containsKey('quota_usage_in_bytes')) {
context.handle(
_quotaUsageInBytesMeta,
quotaUsageInBytes.isAcceptableOrUnknown(
data['quota_usage_in_bytes']!,
_quotaUsageInBytesMeta,
),
);
}
if (data.containsKey('pin_code')) {
context.handle(
_pinCodeMeta,
pinCode.isAcceptableOrUnknown(data['pin_code']!, _pinCodeMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.AuthUserEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.AuthUserEntityData(
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
name: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
email: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}email'],
)!,
isAdmin: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_admin'],
)!,
hasProfileImage: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}has_profile_image'],
)!,
profileChangedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}profile_changed_at'],
)!,
avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}avatar_color'],
)!,
),
quotaSizeInBytes: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}quota_size_in_bytes'],
)!,
quotaUsageInBytes: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}quota_usage_in_bytes'],
)!,
pinCode: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}pin_code'],
),
);
}
@override
$AuthUserEntityTable createAlias(String alias) {
return $AuthUserEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.AvatarColor, int, int> $converteravatarColor =
const i0.EnumIndexConverter<i2.AvatarColor>(i2.AvatarColor.values);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class AuthUserEntityData extends i0.DataClass
implements i0.Insertable<i1.AuthUserEntityData> {
final String id;
final String name;
final String email;
final bool isAdmin;
final bool hasProfileImage;
final DateTime profileChangedAt;
final i2.AvatarColor avatarColor;
final int quotaSizeInBytes;
final int quotaUsageInBytes;
final String? pinCode;
const AuthUserEntityData({
required this.id,
required this.name,
required this.email,
required this.isAdmin,
required this.hasProfileImage,
required this.profileChangedAt,
required this.avatarColor,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
this.pinCode,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['email'] = i0.Variable<String>(email);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage);
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt);
{
map['avatar_color'] = i0.Variable<int>(
i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor),
);
}
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes);
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes);
if (!nullToAbsent || pinCode != null) {
map['pin_code'] = i0.Variable<String>(pinCode);
}
return map;
}
factory AuthUserEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return AuthUserEntityData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
email: serializer.fromJson<String>(json['email']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
hasProfileImage: serializer.fromJson<bool>(json['hasProfileImage']),
profileChangedAt: serializer.fromJson<DateTime>(json['profileChangedAt']),
avatarColor: i1.$AuthUserEntityTable.$converteravatarColor.fromJson(
serializer.fromJson<int>(json['avatarColor']),
),
quotaSizeInBytes: serializer.fromJson<int>(json['quotaSizeInBytes']),
quotaUsageInBytes: serializer.fromJson<int>(json['quotaUsageInBytes']),
pinCode: serializer.fromJson<String?>(json['pinCode']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'email': serializer.toJson<String>(email),
'isAdmin': serializer.toJson<bool>(isAdmin),
'hasProfileImage': serializer.toJson<bool>(hasProfileImage),
'profileChangedAt': serializer.toJson<DateTime>(profileChangedAt),
'avatarColor': serializer.toJson<int>(
i1.$AuthUserEntityTable.$converteravatarColor.toJson(avatarColor),
),
'quotaSizeInBytes': serializer.toJson<int>(quotaSizeInBytes),
'quotaUsageInBytes': serializer.toJson<int>(quotaUsageInBytes),
'pinCode': serializer.toJson<String?>(pinCode),
};
}
i1.AuthUserEntityData copyWith({
String? id,
String? name,
String? email,
bool? isAdmin,
bool? hasProfileImage,
DateTime? profileChangedAt,
i2.AvatarColor? avatarColor,
int? quotaSizeInBytes,
int? quotaUsageInBytes,
i0.Value<String?> pinCode = const i0.Value.absent(),
}) => i1.AuthUserEntityData(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
isAdmin: isAdmin ?? this.isAdmin,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
avatarColor: avatarColor ?? this.avatarColor,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
pinCode: pinCode.present ? pinCode.value : this.pinCode,
);
AuthUserEntityData copyWithCompanion(i1.AuthUserEntityCompanion data) {
return AuthUserEntityData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
email: data.email.present ? data.email.value : this.email,
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
hasProfileImage: data.hasProfileImage.present
? data.hasProfileImage.value
: this.hasProfileImage,
profileChangedAt: data.profileChangedAt.present
? data.profileChangedAt.value
: this.profileChangedAt,
avatarColor: data.avatarColor.present
? data.avatarColor.value
: this.avatarColor,
quotaSizeInBytes: data.quotaSizeInBytes.present
? data.quotaSizeInBytes.value
: this.quotaSizeInBytes,
quotaUsageInBytes: data.quotaUsageInBytes.present
? data.quotaUsageInBytes.value
: this.quotaUsageInBytes,
pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode,
);
}
@override
String toString() {
return (StringBuffer('AuthUserEntityData(')
..write('id: $id, ')
..write('name: $name, ')
..write('email: $email, ')
..write('isAdmin: $isAdmin, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('avatarColor: $avatarColor, ')
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
..write('quotaUsageInBytes: $quotaUsageInBytes, ')
..write('pinCode: $pinCode')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(
id,
name,
email,
isAdmin,
hasProfileImage,
profileChangedAt,
avatarColor,
quotaSizeInBytes,
quotaUsageInBytes,
pinCode,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.AuthUserEntityData &&
other.id == this.id &&
other.name == this.name &&
other.email == this.email &&
other.isAdmin == this.isAdmin &&
other.hasProfileImage == this.hasProfileImage &&
other.profileChangedAt == this.profileChangedAt &&
other.avatarColor == this.avatarColor &&
other.quotaSizeInBytes == this.quotaSizeInBytes &&
other.quotaUsageInBytes == this.quotaUsageInBytes &&
other.pinCode == this.pinCode);
}
class AuthUserEntityCompanion
extends i0.UpdateCompanion<i1.AuthUserEntityData> {
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<String> email;
final i0.Value<bool> isAdmin;
final i0.Value<bool> hasProfileImage;
final i0.Value<DateTime> profileChangedAt;
final i0.Value<i2.AvatarColor> avatarColor;
final i0.Value<int> quotaSizeInBytes;
final i0.Value<int> quotaUsageInBytes;
final i0.Value<String?> pinCode;
const AuthUserEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.email = const i0.Value.absent(),
this.isAdmin = const i0.Value.absent(),
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
this.avatarColor = const i0.Value.absent(),
this.quotaSizeInBytes = const i0.Value.absent(),
this.quotaUsageInBytes = const i0.Value.absent(),
this.pinCode = const i0.Value.absent(),
});
AuthUserEntityCompanion.insert({
required String id,
required String name,
required String email,
this.isAdmin = const i0.Value.absent(),
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
required i2.AvatarColor avatarColor,
this.quotaSizeInBytes = const i0.Value.absent(),
this.quotaUsageInBytes = const i0.Value.absent(),
this.pinCode = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
email = i0.Value(email),
avatarColor = i0.Value(avatarColor);
static i0.Insertable<i1.AuthUserEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<String>? email,
i0.Expression<bool>? isAdmin,
i0.Expression<bool>? hasProfileImage,
i0.Expression<DateTime>? profileChangedAt,
i0.Expression<int>? avatarColor,
i0.Expression<int>? quotaSizeInBytes,
i0.Expression<int>? quotaUsageInBytes,
i0.Expression<String>? pinCode,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (email != null) 'email': email,
if (isAdmin != null) 'is_admin': isAdmin,
if (hasProfileImage != null) 'has_profile_image': hasProfileImage,
if (profileChangedAt != null) 'profile_changed_at': profileChangedAt,
if (avatarColor != null) 'avatar_color': avatarColor,
if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes,
if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes,
if (pinCode != null) 'pin_code': pinCode,
});
}
i1.AuthUserEntityCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<String>? email,
i0.Value<bool>? isAdmin,
i0.Value<bool>? hasProfileImage,
i0.Value<DateTime>? profileChangedAt,
i0.Value<i2.AvatarColor>? avatarColor,
i0.Value<int>? quotaSizeInBytes,
i0.Value<int>? quotaUsageInBytes,
i0.Value<String?>? pinCode,
}) {
return i1.AuthUserEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
isAdmin: isAdmin ?? this.isAdmin,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
avatarColor: avatarColor ?? this.avatarColor,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
pinCode: pinCode ?? this.pinCode,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (email.present) {
map['email'] = i0.Variable<String>(email.value);
}
if (isAdmin.present) {
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
}
if (hasProfileImage.present) {
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage.value);
}
if (profileChangedAt.present) {
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt.value);
}
if (avatarColor.present) {
map['avatar_color'] = i0.Variable<int>(
i1.$AuthUserEntityTable.$converteravatarColor.toSql(avatarColor.value),
);
}
if (quotaSizeInBytes.present) {
map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes.value);
}
if (quotaUsageInBytes.present) {
map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes.value);
}
if (pinCode.present) {
map['pin_code'] = i0.Variable<String>(pinCode.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('AuthUserEntityCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('email: $email, ')
..write('isAdmin: $isAdmin, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('avatarColor: $avatarColor, ')
..write('quotaSizeInBytes: $quotaSizeInBytes, ')
..write('quotaUsageInBytes: $quotaUsageInBytes, ')
..write('pinCode: $pinCode')
..write(')'))
.toString();
}
}

View File

@@ -1,5 +1,7 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
@@ -11,9 +13,26 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
IntColumn get backupSelection => intEnum<BackupSelection>()();
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
// // Linked album for putting assets to the remote album after finished uploading
TextColumn get linkedRemoteAlbumId =>
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.setNull).nullable()();
// Used for mark & sweep
BoolColumn get marker_ => boolean().nullable()();
@override
Set<Column> get primaryKey => {id};
}
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
LocalAlbum toDto({int assetCount = 0}) {
return LocalAlbum(
id: id,
name: name,
updatedAt: updatedAt,
assetCount: assetCount,
backupSelection: backupSelection,
linkedRemoteAlbumId: linkedRemoteAlbumId,
);
}
}

View File

@@ -7,6 +7,9 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
i1.LocalAlbumEntityCompanion Function({
@@ -15,6 +18,7 @@ typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
i0.Value<DateTime> updatedAt,
required i2.BackupSelection backupSelection,
i0.Value<bool> isIosSharedAlbum,
i0.Value<String?> linkedRemoteAlbumId,
i0.Value<bool?> marker_,
});
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
@@ -24,9 +28,57 @@ typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
i0.Value<DateTime> updatedAt,
i0.Value<i2.BackupSelection> backupSelection,
i0.Value<bool> isIosSharedAlbum,
i0.Value<String?> linkedRemoteAlbumId,
i0.Value<bool?> marker_,
});
final class $$LocalAlbumEntityTableReferences
extends
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData
> {
$$LocalAlbumEntityTableReferences(
super.$_db,
super.$_table,
super.$_typedResult,
);
static i5.$RemoteAlbumEntityTable _linkedRemoteAlbumIdTable(
i0.GeneratedDatabase db,
) => i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAlbumEntityTable>('local_album_entity')
.linkedRemoteAlbumId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity').id,
),
);
i5.$$RemoteAlbumEntityTableProcessedTableManager? get linkedRemoteAlbumId {
final $_column = $_itemColumn<String>('linked_remote_album_id');
if ($_column == null) return null;
final manager = i5
.$$RemoteAlbumEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer(
$_db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_linkedRemoteAlbumIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
}
class $$LocalAlbumEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
$$LocalAlbumEntityTableFilterComposer({
@@ -66,6 +118,33 @@ class $$LocalAlbumEntityTableFilterComposer
column: $table.marker_,
builder: (column) => i0.ColumnFilters(column),
);
i5.$$RemoteAlbumEntityTableFilterComposer get linkedRemoteAlbumId {
final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$RemoteAlbumEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalAlbumEntityTableOrderingComposer
@@ -106,6 +185,34 @@ class $$LocalAlbumEntityTableOrderingComposer
column: $table.marker_,
builder: (column) => i0.ColumnOrderings(column),
);
i5.$$RemoteAlbumEntityTableOrderingComposer get linkedRemoteAlbumId {
final i5.$$RemoteAlbumEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$RemoteAlbumEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalAlbumEntityTableAnnotationComposer
@@ -139,6 +246,34 @@ class $$LocalAlbumEntityTableAnnotationComposer
i0.GeneratedColumn<bool> get marker_ =>
$composableBuilder(column: $table.marker_, builder: (column) => column);
i5.$$RemoteAlbumEntityTableAnnotationComposer get linkedRemoteAlbumId {
final i5.$$RemoteAlbumEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$RemoteAlbumEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalAlbumEntityTableTableManager
@@ -152,16 +287,9 @@ class $$LocalAlbumEntityTableTableManager
i1.$$LocalAlbumEntityTableAnnotationComposer,
$$LocalAlbumEntityTableCreateCompanionBuilder,
$$LocalAlbumEntityTableUpdateCompanionBuilder,
(
i1.LocalAlbumEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData
>,
),
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
i1.LocalAlbumEntityData,
i0.PrefetchHooks Function()
i0.PrefetchHooks Function({bool linkedRemoteAlbumId})
> {
$$LocalAlbumEntityTableTableManager(
i0.GeneratedDatabase db,
@@ -187,6 +315,7 @@ class $$LocalAlbumEntityTableTableManager
i0.Value<i2.BackupSelection> backupSelection =
const i0.Value.absent(),
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) => i1.LocalAlbumEntityCompanion(
id: id,
@@ -194,6 +323,7 @@ class $$LocalAlbumEntityTableTableManager
updatedAt: updatedAt,
backupSelection: backupSelection,
isIosSharedAlbum: isIosSharedAlbum,
linkedRemoteAlbumId: linkedRemoteAlbumId,
marker_: marker_,
),
createCompanionCallback:
@@ -203,6 +333,7 @@ class $$LocalAlbumEntityTableTableManager
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
required i2.BackupSelection backupSelection,
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) => i1.LocalAlbumEntityCompanion.insert(
id: id,
@@ -210,12 +341,60 @@ class $$LocalAlbumEntityTableTableManager
updatedAt: updatedAt,
backupSelection: backupSelection,
isIosSharedAlbum: isIosSharedAlbum,
linkedRemoteAlbumId: linkedRemoteAlbumId,
marker_: marker_,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.map(
(e) => (
e.readTable(table),
i1.$$LocalAlbumEntityTableReferences(db, table, e),
),
)
.toList(),
prefetchHooksCallback: null,
prefetchHooksCallback: ({linkedRemoteAlbumId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins:
<
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic
>
>(state) {
if (linkedRemoteAlbumId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.linkedRemoteAlbumId,
referencedTable: i1
.$$LocalAlbumEntityTableReferences
._linkedRemoteAlbumIdTable(db),
referencedColumn: i1
.$$LocalAlbumEntityTableReferences
._linkedRemoteAlbumIdTable(db)
.id,
)
as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
),
);
}
@@ -230,16 +409,9 @@ typedef $$LocalAlbumEntityTableProcessedTableManager =
i1.$$LocalAlbumEntityTableAnnotationComposer,
$$LocalAlbumEntityTableCreateCompanionBuilder,
$$LocalAlbumEntityTableUpdateCompanionBuilder,
(
i1.LocalAlbumEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData
>,
),
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
i1.LocalAlbumEntityData,
i0.PrefetchHooks Function()
i0.PrefetchHooks Function({bool linkedRemoteAlbumId})
>;
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
@@ -308,6 +480,20 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _linkedRemoteAlbumIdMeta =
const i0.VerificationMeta('linkedRemoteAlbumId');
@override
late final i0.GeneratedColumn<String> linkedRemoteAlbumId =
i0.GeneratedColumn<String>(
'linked_remote_album_id',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
),
);
static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta(
'marker_',
);
@@ -329,6 +515,7 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
updatedAt,
backupSelection,
isIosSharedAlbum,
linkedRemoteAlbumId,
marker_,
];
@override
@@ -371,6 +558,15 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
),
);
}
if (data.containsKey('linked_remote_album_id')) {
context.handle(
_linkedRemoteAlbumIdMeta,
linkedRemoteAlbumId.isAcceptableOrUnknown(
data['linked_remote_album_id']!,
_linkedRemoteAlbumIdMeta,
),
);
}
if (data.containsKey('marker')) {
context.handle(
_marker_Meta,
@@ -412,6 +608,10 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
i0.DriftSqlType.bool,
data['${effectivePrefix}is_ios_shared_album'],
)!,
linkedRemoteAlbumId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}linked_remote_album_id'],
),
marker_: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}marker'],
@@ -441,6 +641,7 @@ class LocalAlbumEntityData extends i0.DataClass
final DateTime updatedAt;
final i2.BackupSelection backupSelection;
final bool isIosSharedAlbum;
final String? linkedRemoteAlbumId;
final bool? marker_;
const LocalAlbumEntityData({
required this.id,
@@ -448,6 +649,7 @@ class LocalAlbumEntityData extends i0.DataClass
required this.updatedAt,
required this.backupSelection,
required this.isIosSharedAlbum,
this.linkedRemoteAlbumId,
this.marker_,
});
@override
@@ -464,6 +666,9 @@ class LocalAlbumEntityData extends i0.DataClass
);
}
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum);
if (!nullToAbsent || linkedRemoteAlbumId != null) {
map['linked_remote_album_id'] = i0.Variable<String>(linkedRemoteAlbumId);
}
if (!nullToAbsent || marker_ != null) {
map['marker'] = i0.Variable<bool>(marker_);
}
@@ -482,6 +687,9 @@ class LocalAlbumEntityData extends i0.DataClass
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
isIosSharedAlbum: serializer.fromJson<bool>(json['isIosSharedAlbum']),
linkedRemoteAlbumId: serializer.fromJson<String?>(
json['linkedRemoteAlbumId'],
),
marker_: serializer.fromJson<bool?>(json['marker_']),
);
}
@@ -498,6 +706,7 @@ class LocalAlbumEntityData extends i0.DataClass
),
),
'isIosSharedAlbum': serializer.toJson<bool>(isIosSharedAlbum),
'linkedRemoteAlbumId': serializer.toJson<String?>(linkedRemoteAlbumId),
'marker_': serializer.toJson<bool?>(marker_),
};
}
@@ -508,6 +717,7 @@ class LocalAlbumEntityData extends i0.DataClass
DateTime? updatedAt,
i2.BackupSelection? backupSelection,
bool? isIosSharedAlbum,
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) => i1.LocalAlbumEntityData(
id: id ?? this.id,
@@ -515,6 +725,9 @@ class LocalAlbumEntityData extends i0.DataClass
updatedAt: updatedAt ?? this.updatedAt,
backupSelection: backupSelection ?? this.backupSelection,
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
linkedRemoteAlbumId: linkedRemoteAlbumId.present
? linkedRemoteAlbumId.value
: this.linkedRemoteAlbumId,
marker_: marker_.present ? marker_.value : this.marker_,
);
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
@@ -528,6 +741,9 @@ class LocalAlbumEntityData extends i0.DataClass
isIosSharedAlbum: data.isIosSharedAlbum.present
? data.isIosSharedAlbum.value
: this.isIosSharedAlbum,
linkedRemoteAlbumId: data.linkedRemoteAlbumId.present
? data.linkedRemoteAlbumId.value
: this.linkedRemoteAlbumId,
marker_: data.marker_.present ? data.marker_.value : this.marker_,
);
}
@@ -540,6 +756,7 @@ class LocalAlbumEntityData extends i0.DataClass
..write('updatedAt: $updatedAt, ')
..write('backupSelection: $backupSelection, ')
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ')
..write('marker_: $marker_')
..write(')'))
.toString();
@@ -552,6 +769,7 @@ class LocalAlbumEntityData extends i0.DataClass
updatedAt,
backupSelection,
isIosSharedAlbum,
linkedRemoteAlbumId,
marker_,
);
@override
@@ -563,6 +781,7 @@ class LocalAlbumEntityData extends i0.DataClass
other.updatedAt == this.updatedAt &&
other.backupSelection == this.backupSelection &&
other.isIosSharedAlbum == this.isIosSharedAlbum &&
other.linkedRemoteAlbumId == this.linkedRemoteAlbumId &&
other.marker_ == this.marker_);
}
@@ -573,6 +792,7 @@ class LocalAlbumEntityCompanion
final i0.Value<DateTime> updatedAt;
final i0.Value<i2.BackupSelection> backupSelection;
final i0.Value<bool> isIosSharedAlbum;
final i0.Value<String?> linkedRemoteAlbumId;
final i0.Value<bool?> marker_;
const LocalAlbumEntityCompanion({
this.id = const i0.Value.absent(),
@@ -580,6 +800,7 @@ class LocalAlbumEntityCompanion
this.updatedAt = const i0.Value.absent(),
this.backupSelection = const i0.Value.absent(),
this.isIosSharedAlbum = const i0.Value.absent(),
this.linkedRemoteAlbumId = const i0.Value.absent(),
this.marker_ = const i0.Value.absent(),
});
LocalAlbumEntityCompanion.insert({
@@ -588,6 +809,7 @@ class LocalAlbumEntityCompanion
this.updatedAt = const i0.Value.absent(),
required i2.BackupSelection backupSelection,
this.isIosSharedAlbum = const i0.Value.absent(),
this.linkedRemoteAlbumId = const i0.Value.absent(),
this.marker_ = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
@@ -598,6 +820,7 @@ class LocalAlbumEntityCompanion
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? backupSelection,
i0.Expression<bool>? isIosSharedAlbum,
i0.Expression<String>? linkedRemoteAlbumId,
i0.Expression<bool>? marker_,
}) {
return i0.RawValuesInsertable({
@@ -606,6 +829,8 @@ class LocalAlbumEntityCompanion
if (updatedAt != null) 'updated_at': updatedAt,
if (backupSelection != null) 'backup_selection': backupSelection,
if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum,
if (linkedRemoteAlbumId != null)
'linked_remote_album_id': linkedRemoteAlbumId,
if (marker_ != null) 'marker': marker_,
});
}
@@ -616,6 +841,7 @@ class LocalAlbumEntityCompanion
i0.Value<DateTime>? updatedAt,
i0.Value<i2.BackupSelection>? backupSelection,
i0.Value<bool>? isIosSharedAlbum,
i0.Value<String?>? linkedRemoteAlbumId,
i0.Value<bool?>? marker_,
}) {
return i1.LocalAlbumEntityCompanion(
@@ -624,6 +850,7 @@ class LocalAlbumEntityCompanion
updatedAt: updatedAt ?? this.updatedAt,
backupSelection: backupSelection ?? this.backupSelection,
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
marker_: marker_ ?? this.marker_,
);
}
@@ -650,6 +877,11 @@ class LocalAlbumEntityCompanion
if (isIosSharedAlbum.present) {
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum.value);
}
if (linkedRemoteAlbumId.present) {
map['linked_remote_album_id'] = i0.Variable<String>(
linkedRemoteAlbumId.value,
);
}
if (marker_.present) {
map['marker'] = i0.Variable<bool>(marker_.value);
}
@@ -664,6 +896,7 @@ class LocalAlbumEntityCompanion
..write('updatedAt: $updatedAt, ')
..write('backupSelection: $backupSelection, ')
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ')
..write('marker_: $marker_')
..write(')'))
.toString();

View File

@@ -20,7 +20,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
Set<Column> get primaryKey => {id};
}
extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
LocalAsset toDto() => LocalAsset(
id: id,
name: name,

View File

@@ -1,6 +1,5 @@
import 'package:drift/drift.dart' hide Index;
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
@@ -44,7 +43,7 @@ class User {
static User fromDto(UserDto dto) => User(
id: dto.id,
updatedAt: dto.updatedAt,
updatedAt: dto.updatedAt ?? DateTime(2025),
email: dto.email,
name: dto.name,
isAdmin: dto.isAdmin,
@@ -81,13 +80,12 @@ class UserEntity extends Table with DriftDefaultsMixin {
TextColumn get id => text()();
TextColumn get name => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
TextColumn get email => text()();
// Profile image
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get avatarColor => intEnum<AvatarColor>().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {id};

View File

@@ -3,28 +3,27 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/domain/models/user.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$UserEntityTableCreateCompanionBuilder =
i1.UserEntityCompanion Function({
required String id,
required String name,
i0.Value<bool> isAdmin,
required String email,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
i0.Value<DateTime> updatedAt,
i0.Value<i2.AvatarColor> avatarColor,
});
typedef $$UserEntityTableUpdateCompanionBuilder =
i1.UserEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> name,
i0.Value<bool> isAdmin,
i0.Value<String> email,
i0.Value<bool> hasProfileImage,
i0.Value<DateTime> profileChangedAt,
i0.Value<DateTime> updatedAt,
i0.Value<i2.AvatarColor> avatarColor,
});
class $$UserEntityTableFilterComposer
@@ -46,11 +45,6 @@ class $$UserEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnFilters(column),
@@ -66,9 +60,10 @@ class $$UserEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
i0.ColumnWithTypeConverterFilters<i2.AvatarColor, i2.AvatarColor, int>
get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
}
@@ -91,11 +86,6 @@ class $$UserEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
column: $table.isAdmin,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get email => $composableBuilder(
column: $table.email,
builder: (column) => i0.ColumnOrderings(column),
@@ -111,8 +101,8 @@ class $$UserEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
i0.ColumnOrderings<int> get avatarColor => $composableBuilder(
column: $table.avatarColor,
builder: (column) => i0.ColumnOrderings(column),
);
}
@@ -132,9 +122,6 @@ class $$UserEntityTableAnnotationComposer
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumn<bool> get isAdmin =>
$composableBuilder(column: $table.isAdmin, builder: (column) => column);
i0.GeneratedColumn<String> get email =>
$composableBuilder(column: $table.email, builder: (column) => column);
@@ -148,8 +135,11 @@ class $$UserEntityTableAnnotationComposer
builder: (column) => column,
);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int> get avatarColor =>
$composableBuilder(
column: $table.avatarColor,
builder: (column) => column,
);
}
class $$UserEntityTableTableManager
@@ -191,37 +181,33 @@ class $$UserEntityTableTableManager
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
}) => i1.UserEntityCompanion(
id: id,
name: name,
isAdmin: isAdmin,
email: email,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
updatedAt: updatedAt,
avatarColor: avatarColor,
),
createCompanionCallback:
({
required String id,
required String name,
i0.Value<bool> isAdmin = const i0.Value.absent(),
required String email,
i0.Value<bool> hasProfileImage = const i0.Value.absent(),
i0.Value<DateTime> profileChangedAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<i2.AvatarColor> avatarColor = const i0.Value.absent(),
}) => i1.UserEntityCompanion.insert(
id: id,
name: name,
isAdmin: isAdmin,
email: email,
hasProfileImage: hasProfileImage,
profileChangedAt: profileChangedAt,
updatedAt: updatedAt,
avatarColor: avatarColor,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -253,7 +239,7 @@ typedef $$UserEntityTableProcessedTableManager =
i0.PrefetchHooks Function()
>;
class $UserEntityTable extends i2.UserEntity
class $UserEntityTable extends i3.UserEntity
with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@@ -279,21 +265,6 @@ class $UserEntityTable extends i2.UserEntity
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _isAdminMeta = const i0.VerificationMeta(
'isAdmin',
);
@override
late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
'is_admin',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_admin" IN (0, 1))',
),
defaultValue: const i3.Constant(false),
);
static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta(
'email',
);
@@ -318,7 +289,7 @@ class $UserEntityTable extends i2.UserEntity
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("has_profile_image" IN (0, 1))',
),
defaultValue: const i3.Constant(false),
defaultValue: const i4.Constant(false),
);
static const i0.VerificationMeta _profileChangedAtMeta =
const i0.VerificationMeta('profileChangedAt');
@@ -330,30 +301,26 @@ class $UserEntityTable extends i2.UserEntity
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
defaultValue: i4.currentDateAndTime,
);
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt',
);
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
late final i0.GeneratedColumnWithTypeConverter<i2.AvatarColor, int>
avatarColor = i0.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
).withConverter<i2.AvatarColor>(i1.$UserEntityTable.$converteravatarColor);
@override
List<i0.GeneratedColumn> get $columns => [
id,
name,
isAdmin,
email,
hasProfileImage,
profileChangedAt,
updatedAt,
avatarColor,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -380,12 +347,6 @@ class $UserEntityTable extends i2.UserEntity
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('is_admin')) {
context.handle(
_isAdminMeta,
isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta),
);
}
if (data.containsKey('email')) {
context.handle(
_emailMeta,
@@ -412,12 +373,6 @@ class $UserEntityTable extends i2.UserEntity
),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@@ -435,10 +390,6 @@ class $UserEntityTable extends i2.UserEntity
i0.DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
isAdmin: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_admin'],
)!,
email: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}email'],
@@ -451,10 +402,12 @@ class $UserEntityTable extends i2.UserEntity
i0.DriftSqlType.dateTime,
data['${effectivePrefix}profile_changed_at'],
)!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
avatarColor: i1.$UserEntityTable.$converteravatarColor.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}avatar_color'],
)!,
),
);
}
@@ -463,6 +416,8 @@ class $UserEntityTable extends i2.UserEntity
return $UserEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.AvatarColor, int, int> $converteravatarColor =
const i0.EnumIndexConverter<i2.AvatarColor>(i2.AvatarColor.values);
@override
bool get withoutRowId => true;
@override
@@ -473,30 +428,31 @@ class UserEntityData extends i0.DataClass
implements i0.Insertable<i1.UserEntityData> {
final String id;
final String name;
final bool isAdmin;
final String email;
final bool hasProfileImage;
final DateTime profileChangedAt;
final DateTime updatedAt;
final i2.AvatarColor avatarColor;
const UserEntityData({
required this.id,
required this.name,
required this.isAdmin,
required this.email,
required this.hasProfileImage,
required this.profileChangedAt,
required this.updatedAt,
required this.avatarColor,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['email'] = i0.Variable<String>(email);
map['has_profile_image'] = i0.Variable<bool>(hasProfileImage);
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
{
map['avatar_color'] = i0.Variable<int>(
i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor),
);
}
return map;
}
@@ -508,11 +464,12 @@ class UserEntityData extends i0.DataClass
return UserEntityData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
email: serializer.fromJson<String>(json['email']),
hasProfileImage: serializer.fromJson<bool>(json['hasProfileImage']),
profileChangedAt: serializer.fromJson<DateTime>(json['profileChangedAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
avatarColor: i1.$UserEntityTable.$converteravatarColor.fromJson(
serializer.fromJson<int>(json['avatarColor']),
),
);
}
@override
@@ -521,36 +478,34 @@ class UserEntityData extends i0.DataClass
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'isAdmin': serializer.toJson<bool>(isAdmin),
'email': serializer.toJson<String>(email),
'hasProfileImage': serializer.toJson<bool>(hasProfileImage),
'profileChangedAt': serializer.toJson<DateTime>(profileChangedAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'avatarColor': serializer.toJson<int>(
i1.$UserEntityTable.$converteravatarColor.toJson(avatarColor),
),
};
}
i1.UserEntityData copyWith({
String? id,
String? name,
bool? isAdmin,
String? email,
bool? hasProfileImage,
DateTime? profileChangedAt,
DateTime? updatedAt,
i2.AvatarColor? avatarColor,
}) => i1.UserEntityData(
id: id ?? this.id,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
email: email ?? this.email,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
updatedAt: updatedAt ?? this.updatedAt,
avatarColor: avatarColor ?? this.avatarColor,
);
UserEntityData copyWithCompanion(i1.UserEntityCompanion data) {
return UserEntityData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
email: data.email.present ? data.email.value : this.email,
hasProfileImage: data.hasProfileImage.present
? data.hasProfileImage.value
@@ -558,7 +513,9 @@ class UserEntityData extends i0.DataClass
profileChangedAt: data.profileChangedAt.present
? data.profileChangedAt.value
: this.profileChangedAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
avatarColor: data.avatarColor.present
? data.avatarColor.value
: this.avatarColor,
);
}
@@ -567,11 +524,10 @@ class UserEntityData extends i0.DataClass
return (StringBuffer('UserEntityData(')
..write('id: $id, ')
..write('name: $name, ')
..write('isAdmin: $isAdmin, ')
..write('email: $email, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('updatedAt: $updatedAt')
..write('avatarColor: $avatarColor')
..write(')'))
.toString();
}
@@ -580,11 +536,10 @@ class UserEntityData extends i0.DataClass
int get hashCode => Object.hash(
id,
name,
isAdmin,
email,
hasProfileImage,
profileChangedAt,
updatedAt,
avatarColor,
);
@override
bool operator ==(Object other) =>
@@ -592,78 +547,70 @@ class UserEntityData extends i0.DataClass
(other is i1.UserEntityData &&
other.id == this.id &&
other.name == this.name &&
other.isAdmin == this.isAdmin &&
other.email == this.email &&
other.hasProfileImage == this.hasProfileImage &&
other.profileChangedAt == this.profileChangedAt &&
other.updatedAt == this.updatedAt);
other.avatarColor == this.avatarColor);
}
class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<bool> isAdmin;
final i0.Value<String> email;
final i0.Value<bool> hasProfileImage;
final i0.Value<DateTime> profileChangedAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<i2.AvatarColor> avatarColor;
const UserEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.isAdmin = const i0.Value.absent(),
this.email = const i0.Value.absent(),
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.avatarColor = const i0.Value.absent(),
});
UserEntityCompanion.insert({
required String id,
required String name,
this.isAdmin = const i0.Value.absent(),
required String email,
this.hasProfileImage = const i0.Value.absent(),
this.profileChangedAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.avatarColor = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
email = i0.Value(email);
static i0.Insertable<i1.UserEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<bool>? isAdmin,
i0.Expression<String>? email,
i0.Expression<bool>? hasProfileImage,
i0.Expression<DateTime>? profileChangedAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? avatarColor,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (isAdmin != null) 'is_admin': isAdmin,
if (email != null) 'email': email,
if (hasProfileImage != null) 'has_profile_image': hasProfileImage,
if (profileChangedAt != null) 'profile_changed_at': profileChangedAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (avatarColor != null) 'avatar_color': avatarColor,
});
}
i1.UserEntityCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<bool>? isAdmin,
i0.Value<String>? email,
i0.Value<bool>? hasProfileImage,
i0.Value<DateTime>? profileChangedAt,
i0.Value<DateTime>? updatedAt,
i0.Value<i2.AvatarColor>? avatarColor,
}) {
return i1.UserEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
email: email ?? this.email,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
updatedAt: updatedAt ?? this.updatedAt,
avatarColor: avatarColor ?? this.avatarColor,
);
}
@@ -676,9 +623,6 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (isAdmin.present) {
map['is_admin'] = i0.Variable<bool>(isAdmin.value);
}
if (email.present) {
map['email'] = i0.Variable<String>(email.value);
}
@@ -688,8 +632,10 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
if (profileChangedAt.present) {
map['profile_changed_at'] = i0.Variable<DateTime>(profileChangedAt.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
if (avatarColor.present) {
map['avatar_color'] = i0.Variable<int>(
i1.$UserEntityTable.$converteravatarColor.toSql(avatarColor.value),
);
}
return map;
}
@@ -699,11 +645,10 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
return (StringBuffer('UserEntityCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('isAdmin: $isAdmin, ')
..write('email: $email, ')
..write('hasProfileImage: $hasProfileImage, ')
..write('profileChangedAt: $profileChangedAt, ')
..write('updatedAt: $updatedAt')
..write('avatarColor: $avatarColor')
..write(')'))
.toString();
}

View File

@@ -4,9 +4,10 @@ import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import "package:immich_mobile/utils/database.utils.dart";
final backupRepositoryProvider = Provider<DriftBackupRepository>(
(ref) => DriftBackupRepository(ref.watch(driftProvider)),

View File

@@ -5,6 +5,7 @@ import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
@@ -43,6 +44,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
@DriftDatabase(
tables: [
AuthUserEntity,
UserEntity,
UserMetadataEntity,
PartnerEntity,
@@ -64,11 +66,17 @@ class IsarDatabaseRepository implements IDatabaseRepository {
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
)
class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor])
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
Drift({QueryExecutor? executor, bool shareAcrossIsolates = true})
: super(
executor ??
driftDatabase(
name: 'immich',
native: DriftNativeOptions(shareAcrossIsolates: shareAcrossIsolates),
),
);
@override
int get schemaVersion => 8;
int get schemaVersion => 10;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -123,6 +131,14 @@ class Drift extends $Drift implements IDatabaseRepository {
from7To8: (m, v8) async {
await m.create(v8.storeEntity);
},
from8To9: (m, v9) async {
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
},
from9To10: (m, v10) async {
await m.createTable(v10.authUserEntity);
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
await m.alterTable(TableMigration(v10.userEntity));
},
),
);
@@ -138,7 +154,7 @@ class Drift extends $Drift implements IDatabaseRepository {
await customStatement('PRAGMA foreign_keys = ON');
await customStatement('PRAGMA synchronous = NORMAL');
await customStatement('PRAGMA journal_mode = WAL');
await customStatement('PRAGMA busy_timeout = 500');
await customStatement('PRAGMA busy_timeout = 30000');
},
);
}

View File

@@ -9,35 +9,37 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
as i3;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i4;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i5;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i6;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i7;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i8;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i9;
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
as i5;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i6;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i7;
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
as i8;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i9;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i11;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
as i12;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i14;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i15;
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i16;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
as i17;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i18;
import 'package:drift/internal/modular.dart' as i19;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i19;
import 'package:drift/internal/modular.dart' as i20;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -48,33 +50,36 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this);
late final i4.$LocalAssetEntityTable localAssetEntity = i4
.$LocalAssetEntityTable(this);
late final i5.$LocalAlbumEntityTable localAlbumEntity = i5
late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5
.$RemoteAlbumEntityTable(this);
late final i6.$LocalAlbumEntityTable localAlbumEntity = i6
.$LocalAlbumEntityTable(this);
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i6
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
.$LocalAlbumAssetEntityTable(this);
late final i7.$UserMetadataEntityTable userMetadataEntity = i7
.$UserMetadataEntityTable(this);
late final i8.$PartnerEntityTable partnerEntity = i8.$PartnerEntityTable(
late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable(
this,
);
late final i9.$RemoteExifEntityTable remoteExifEntity = i9
.$RemoteExifEntityTable(this);
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = i10
.$RemoteAlbumEntityTable(this);
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11
.$RemoteAlbumAssetEntityTable(this);
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12
.$RemoteAlbumUserEntityTable(this);
late final i13.$MemoryEntityTable memoryEntity = i13.$MemoryEntityTable(this);
late final i14.$MemoryAssetEntityTable memoryAssetEntity = i14
.$MemoryAssetEntityTable(this);
late final i15.$PersonEntityTable personEntity = i15.$PersonEntityTable(this);
late final i16.$AssetFaceEntityTable assetFaceEntity = i16
.$AssetFaceEntityTable(this);
late final i17.$StoreEntityTable storeEntity = i17.$StoreEntityTable(this);
i18.MergedAssetDrift get mergedAssetDrift => i19.ReadDatabaseContainer(
late final i9.$UserMetadataEntityTable userMetadataEntity = i9
.$UserMetadataEntityTable(this);
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable(
this,
).accessor<i18.MergedAssetDrift>(i18.MergedAssetDrift.new);
);
late final i11.$RemoteExifEntityTable remoteExifEntity = i11
.$RemoteExifEntityTable(this);
late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12
.$RemoteAlbumAssetEntityTable(this);
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
.$RemoteAlbumUserEntityTable(this);
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this);
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15
.$MemoryAssetEntityTable(this);
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
late final i17.$AssetFaceEntityTable assetFaceEntity = i17
.$AssetFaceEntityTable(this);
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this);
i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer(
this,
).accessor<i19.MergedAssetDrift>(i19.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -84,6 +89,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
i4.idxLocalAssetChecksum,
@@ -91,10 +97,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum,
i2.idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
@@ -102,7 +108,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
personEntity,
assetFaceEntity,
storeEntity,
i9.idxLatLng,
i11.idxLatLng,
];
@override
i0.StreamQueryUpdateRules
@@ -123,6 +129,33 @@ abstract class $Drift extends i0.GeneratedDatabase {
),
result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'user_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_album_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('local_album_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'local_asset_entity',
@@ -173,24 +206,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'user_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
@@ -290,33 +305,35 @@ class $DriftManager {
i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i7.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i7.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i8.$$PartnerEntityTableTableManager get partnerEntity =>
i8.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i11.$$RemoteAlbumAssetEntityTableTableManager(
i8.$$AuthUserEntityTableTableManager get authUserEntity =>
i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
i9.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i10.$$PartnerEntityTableTableManager get partnerEntity =>
i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i11.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
i12.$$RemoteAlbumAssetEntityTableTableManager(
_db,
_db.remoteAlbumAssetEntity,
);
i12.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i12
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i13.$$MemoryEntityTableTableManager get memoryEntity =>
i13.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i14.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i14.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i15.$$PersonEntityTableTableManager get personEntity =>
i15.$$PersonEntityTableTableManager(_db, _db.personEntity);
i16.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i16.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i17.$$StoreEntityTableTableManager get storeEntity =>
i17.$$StoreEntityTableTableManager(_db, _db.storeEntity);
i14.$$MemoryEntityTableTableManager get memoryEntity =>
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i16.$$PersonEntityTableTableManager get personEntity =>
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i18.$$StoreEntityTableTableManager get storeEntity =>
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity);
}

View File

@@ -3435,6 +3435,841 @@ i1.GeneratedColumn<int> _column_89(String aliasedName) =>
true,
type: i1.DriftSqlType.int,
);
final class Schema9 extends i0.VersionedSchema {
Schema9({required super.database}) : super(version: 9);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
idxLatLng,
];
late final Shape16 userEntity = Shape16(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_84,
_column_85,
_column_5,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape2 localAssetEntity = Shape2(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 localAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
class Shape19 extends i0.VersionedTable {
Shape19({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get backupSelection =>
columnsByName['backup_selection']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get isIosSharedAlbum =>
columnsByName['is_ios_shared_album']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get linkedRemoteAlbumId =>
columnsByName['linked_remote_album_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get marker_ =>
columnsByName['marker']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_90(String aliasedName) =>
i1.GeneratedColumn<String>(
'linked_remote_album_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
),
);
final class Schema10 extends i0.VersionedSchema {
Schema10({required super.database}) : super(version: 10);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
idxLatLng,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape2 localAssetEntity = Shape2(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 localAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
class Shape20 extends i0.VersionedTable {
Shape20({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get email =>
columnsByName['email']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get hasProfileImage =>
columnsByName['has_profile_image']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get profileChangedAt =>
columnsByName['profile_changed_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get avatarColor =>
columnsByName['avatar_color']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_91(String aliasedName) =>
i1.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
class Shape21 extends i0.VersionedTable {
Shape21({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get email =>
columnsByName['email']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isAdmin =>
columnsByName['is_admin']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get hasProfileImage =>
columnsByName['has_profile_image']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get profileChangedAt =>
columnsByName['profile_changed_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get avatarColor =>
columnsByName['avatar_color']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get quotaSizeInBytes =>
columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get quotaUsageInBytes =>
columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get pinCode =>
columnsByName['pin_code']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_92(String aliasedName) =>
i1.GeneratedColumn<int>(
'avatar_color',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<int> _column_93(String aliasedName) =>
i1.GeneratedColumn<int>(
'quota_size_in_bytes',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<String> _column_94(String aliasedName) =>
i1.GeneratedColumn<String>(
'pin_code',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -3443,6 +4278,8 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -3481,6 +4318,16 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from7To8(migrator, schema);
return 8;
case 8:
final schema = Schema9(database: database);
final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema);
return 9;
case 9:
final schema = Schema10(database: database);
final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema);
return 10;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -3495,6 +4342,8 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -3504,5 +4353,7 @@ i1.OnUpgrade stepByStep({
from5To6: from5To6,
from6To7: from6To7,
from7To8: from7To8,
from8To9: from8To9,
from9To10: from9To10,
),
);

View File

@@ -1,11 +1,12 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/utils/database.utils.dart';
import 'package:platform/platform.dart';
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
@@ -49,6 +50,13 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
}
Future<List<LocalAlbum>> getBackupAlbums() async {
final query = _db.localAlbumEntity.select()
..where((row) => row.backupSelection.equalsValue(BackupSelection.selected));
return query.map((row) => row.toDto()).get();
}
Future<void> delete(String albumId) => transaction(() async {
// Remove all assets that are only in this particular album
// We cannot remove all assets in the album because they might be in other albums in iOS
@@ -335,4 +343,16 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
Future<int> getCount() {
return _db.managers.localAlbumEntity.count();
}
Future unlinkRemoteAlbum(String id) async {
return _db.localAlbumEntity.update()
..where((row) => row.id.equals(id))
..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
}
Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
return _db.localAlbumEntity.update()
..where((row) => row.id.equals(localAlbumId))
..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
}
}

View File

@@ -7,9 +7,13 @@ import 'logger_db.repository.drift.dart';
@DriftDatabase(tables: [LogMessageEntity])
class DriftLogger extends $DriftLogger implements IDatabaseRepository {
DriftLogger([QueryExecutor? executor])
DriftLogger({QueryExecutor? executor, bool shareAcrossIsolates = true})
: super(
executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)),
executor ??
driftDatabase(
name: 'immich_logs',
native: DriftNativeOptions(shareAcrossIsolates: shareAcrossIsolates),
),
);
@override

View File

@@ -113,6 +113,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.getSingleOrNull();
}
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
final query = _db.remoteAlbumEntity.select()
..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId))
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(1);
return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull();
}
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
await _db.transaction(() async {
final entity = RemoteAlbumEntityCompanion(
@@ -193,14 +202,13 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
id: user.id,
email: user.email,
name: user.name,
isAdmin: user.isAdmin,
updatedAt: user.updatedAt,
memoryEnabled: true,
inTimeline: false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
profileChangedAt: user.profileChangedAt,
hasProfileImage: user.hasProfileImage,
avatarColor: user.avatarColor,
),
)
.get();
@@ -321,6 +329,42 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
Future<int> getCount() {
return _db.managers.remoteAlbumEntity.count();
}
Future<List<String>> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async {
// Find remote asset ids that:
// 1. Belong to the provided local album (via local_album_asset_entity)
// 2. Have been uploaded (i.e. a matching remote asset exists for the same checksum & owner)
// 3. Are NOT already in the remote album (remote_album_asset_entity)
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id])
..join([
innerJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
),
// Left join remote album assets to exclude those already in the remote album
leftOuterJoin(
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id) &
_db.remoteAlbumAssetEntity.albumId.equals(remoteAlbumId),
useColumns: false,
),
])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.localAlbumAssetEntity.albumId.equals(localAlbumId) &
_db.remoteAlbumAssetEntity.assetId.isNull(), // only those not yet linked
);
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
}
}
extension on RemoteAlbumEntityData {

View File

@@ -16,6 +16,13 @@ class StorageRepository {
file = await entity?.originFile;
if (file == null) {
log.warning("Cannot get file for asset $assetId");
return null;
}
final exists = await file.exists();
if (!exists) {
log.warning("File for asset $assetId does not exist");
return null;
}
} catch (error, stackTrace) {
log.warning("Error getting file for asset $assetId", error, stackTrace);
@@ -34,6 +41,13 @@ class StorageRepository {
log.warning(
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
);
return null;
}
final exists = await file.exists();
if (!exists) {
log.warning("Motion file for asset ${asset.id} does not exist");
return null;
}
} catch (error, stackTrace) {
log.warning(

View File

@@ -173,7 +173,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
const (bool) => entity.intValue == 1,
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) =>
entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!),
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
_ => null,
}
as T?;
@@ -184,7 +184,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id),
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id),
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
};
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));

View File

@@ -18,7 +18,8 @@ class SyncApiRepository {
}
Future<void> streamChanges(
Function(List<SyncEvent>, Function() abort) onData, {
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
Function()? onReset,
int batchSize = kSyncEventBatchSize,
http.Client? httpClient,
}) async {
@@ -37,6 +38,7 @@ class SyncApiRepository {
request.body = jsonEncode(
SyncStreamDto(
types: [
SyncRequestType.authUsersV1,
SyncRequestType.usersV1,
SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1,
@@ -69,6 +71,8 @@ class SyncApiRepository {
shouldAbort = true;
}
final reset = onReset ?? () {};
try {
final response = await client.send(request);
@@ -91,12 +95,12 @@ class SyncApiRepository {
continue;
}
await onData(_parseLines(lines), abort);
await onData(_parseLines(lines), abort, reset);
lines.clear();
}
if (lines.isNotEmpty && !shouldAbort) {
await onData(_parseLines(lines), abort);
await onData(_parseLines(lines), abort, reset);
}
} catch (error, stack) {
_logger.severe("Error processing stream", error, stack);
@@ -130,6 +134,7 @@ class SyncApiRepository {
}
const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.authUserV1: SyncAuthUserV1.fromJson,
SyncEntityType.userV1: SyncUserV1.fromJson,
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
@@ -156,7 +161,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
SyncEntityType.syncAckV1: _SyncEmptyDto.fromJson,
SyncEntityType.syncResetV1: _SyncEmptyDto.fromJson,
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
@@ -172,8 +178,9 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson,
SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson,
SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson,
SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson,
};
class _SyncAckV1 {
static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1();
class _SyncEmptyDto {
static _SyncEmptyDto? fromJson(dynamic _) => _SyncEmptyDto();
}

View File

@@ -1,11 +1,14 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
@@ -29,6 +32,65 @@ class SyncStreamRepository extends DriftDatabaseRepository {
SyncStreamRepository(super.db) : _db = db;
Future<void> reset() async {
_logger.fine("SyncResetV1 received. Resetting remote entities");
try {
await _db.exclusively(() async {
// foreign_keys PRAGMA is no-op within transactions
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
await _db.customStatement('PRAGMA foreign_keys = OFF');
await transaction(() async {
await _db.assetFaceEntity.deleteAll();
await _db.memoryAssetEntity.deleteAll();
await _db.memoryEntity.deleteAll();
await _db.partnerEntity.deleteAll();
await _db.personEntity.deleteAll();
await _db.remoteAlbumAssetEntity.deleteAll();
await _db.remoteAlbumEntity.deleteAll();
await _db.remoteAlbumUserEntity.deleteAll();
await _db.remoteAssetEntity.deleteAll();
await _db.remoteExifEntity.deleteAll();
await _db.stackEntity.deleteAll();
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
});
await _db.customStatement('PRAGMA foreign_keys = ON');
});
} catch (error, stack) {
_logger.severe('Error: SyncResetV1', error, stack);
rethrow;
}
}
Future<void> updateAuthUsersV1(Iterable<SyncAuthUserV1> data) async {
try {
await _db.batch((batch) {
for (final user in data) {
final companion = AuthUserEntityCompanion(
name: Value(user.name),
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
isAdmin: Value(user.isAdmin),
pinCode: Value(user.pinCode),
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
quotaUsageInBytes: Value(user.quotaUsageInBytes),
);
batch.insert(
_db.authUserEntity,
companion.copyWith(id: Value(user.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: SyncAuthUserV1', error, stack);
rethrow;
}
}
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try {
await _db.userEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId)));
@@ -47,6 +109,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
);
batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion));
@@ -573,3 +636,7 @@ extension on String {
}
}
}
extension on UserAvatarColor {
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
}

View File

@@ -2,8 +2,8 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart';
import 'package:isar/isar.dart';
@@ -68,12 +68,12 @@ class IsarUserRepository extends IsarDatabaseRepository {
}
}
class DriftUserRepository extends DriftDatabaseRepository {
class DriftAuthUserRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftUserRepository(super.db) : _db = db;
const DriftAuthUserRepository(super.db) : _db = db;
Future<UserDto?> get(String id) async {
final user = await _db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
if (user == null) return null;
@@ -84,43 +84,30 @@ class DriftUserRepository extends DriftDatabaseRepository {
}
Future<UserDto> upsert(UserDto user) async {
await _db.userEntity.insertOnConflictUpdate(
UserEntityCompanion(
await _db.authUserEntity.insertOnConflictUpdate(
AuthUserEntityCompanion(
id: Value(user.id),
isAdmin: Value(user.isAdmin),
updatedAt: Value(user.updatedAt),
name: Value(user.name),
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
isAdmin: Value(user.isAdmin),
quotaSizeInBytes: Value(user.quotaSizeInBytes),
quotaUsageInBytes: Value(user.quotaUsageInBytes),
avatarColor: Value(user.avatarColor),
),
);
return user;
}
Future<List<UserDto>> getAll() async {
final users = await _db.userEntity.select().get();
final List<UserDto> result = [];
for (final user in users) {
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id));
final metadata = await query.map((row) => row.toDto()).get();
result.add(user.toDto(metadata));
}
return result;
}
}
extension on UserEntityData {
extension on AuthUserEntityData {
UserDto toDto([List<UserMetadata>? metadata]) {
AvatarColor avatarColor = AvatarColor.primary;
bool memoryEnabled = true;
if (metadata != null) {
for (final meta in metadata) {
if (meta.key == UserMetadataKey.preferences && meta.preferences != null) {
avatarColor = meta.preferences?.userAvatarColor ?? AvatarColor.primary;
memoryEnabled = meta.preferences?.memoriesEnabled ?? true;
}
}
@@ -130,12 +117,13 @@ extension on UserEntityData {
id: id,
email: email,
name: name,
isAdmin: isAdmin,
updatedAt: updatedAt,
profileChangedAt: profileChangedAt,
hasProfileImage: hasProfileImage,
avatarColor: avatarColor,
memoryEnabled: memoryEnabled,
isAdmin: isAdmin,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
);
}
}

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