Compare commits

..

28 Commits

Author SHA1 Message Date
Yaros 2db907239f fix: update ocr & faces after asset edit 2026-06-24 13:18:42 +02:00
shenlong 9d6c219276 fix: current viewer asset reactivity (#29282)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-23 18:40:24 +00:00
shenlong f29f86542c feat: partner actions (#29281)
* feat: partner actions

# Conflicts:
#	i18n/en.json

* cleanup

* fix tests

* ci fix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-23 23:50:59 +05:30
okxint 5165cf1d2f fix(mobile): force AssetViewerPage recreation on repeated view intents (#29235)
* fix(mobile): force AssetViewerPage recreation on repeated view intents

When View in Immich is triggered a second time while the viewer is
already open, auto_route's replaceAll reuses the existing route (same
type, null key) and Flutter keeps the old ConsumerState alive. The
PageController and preloader inside _AssetViewerState are late final,
so they never reset — the viewer stays frozen on the previous asset.

Passing UniqueKey() to AssetViewerRoute ensures each view intent
creates a fresh widget element, so initState runs, the PageController
is initialised from scratch, and the new TimelineService from the
updated ProviderScope override is picked up correctly.

Fixes #29230

* clean up

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2026-06-23 17:43:56 +00:00
shenlong f4c8459484 feat: mobile actions (#29280)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-23 16:50:57 +00:00
Mees Frensel 22ec449e43 chore: remove unused i18n strings (#29288) 2026-06-23 18:02:29 +02:00
Weblate (bot) 0b1019c344 chore(web): update translations (#29204)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de_CH/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fil/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gsw/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ne/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ur/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translation: Immich/immich

Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: AntonPalmqvist <apq@users.noreply.hosted.weblate.org>
Co-authored-by: Benjamin Kunz <benjamin.kunz.ch@gmail.com>
Co-authored-by: Cohinem <twitch9ofe@gmail.com>
Co-authored-by: Conrad Menz <weblate@spamstopover.de>
Co-authored-by: Damian Krysta <damian@krysta.dev>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Edmundas <edmius@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Insoo Seok <tocoyo@gmail.com>
Co-authored-by: Jayden Lo <jaydenlo08@gmail.com>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: Melih Ozkan <malihozkan156@gmail.com>
Co-authored-by: Mr.Biswas <kallan.biswas001@gmail.com>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nicolas Ceballos <nicoshafes@gmail.com>
Co-authored-by: Para <ahh-produktivitet@tutanota.com>
Co-authored-by: Piero B. <biagini93@gmail.com>
Co-authored-by: PierreLapolla <plapolla9@gmail.com>
Co-authored-by: Ronnel <misc.woe@outlook.ph>
Co-authored-by: Sakib Iqbal <sakib.iqbal@gmail.com>
Co-authored-by: Saugat Tripathi <saugat.tripathi76@gmail.com>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Umair Jibran <wablate@umairjibran.com>
Co-authored-by: Unimpeded Lemur <yg7lh0fz3@mozmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vitor Coelho <vitorc195@gmail.com>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: muysup <79565421+MuySup@users.noreply.github.com>
Co-authored-by: Òscar Casajuana <elboletaire@underave.net>
2026-06-23 15:50:30 +00:00
Mees Frensel 06f3b4f259 refactor(web): simple actions (#29257) 2026-06-23 17:08:46 +02:00
Ben Beckford 99f94a363d chore(web): workflow property ordering (#29261)
* chore(web): workflow property ordering

* chore(web): extract schema property sorting to method
2026-06-23 13:03:33 +00:00
Daniel Dietzler c3092b1c2c chore: basque was missing on mobile (#29284) 2026-06-23 08:57:05 -04:00
renovate[bot] 0656e7e231 chore(deps): update dependency typescript to v6 (#28772)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-06-23 14:01:09 +02:00
renovate[bot] 1692b81b7c chore(deps): lock file maintenance (pub) (#28733) 2026-06-23 12:43:35 +02:00
renovate[bot] ff2028c4c8 chore(deps): update prom/prometheus docker digest to a75c5a3 (#29271) 2026-06-23 12:43:16 +02:00
Timon f22836e1bf refactor(server): describe check upload id as string (#29274) 2026-06-23 12:42:42 +02:00
renovate[bot] 7dd02ffbad chore(deps): update github-actions (#29272)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2026-06-23 08:49:21 +00:00
shenlong e51c4cb355 feat: column button (#29265)
* refactor: icon buttons implicit loading

* chore: cleanup

* feat: ui color override

* feat: column button

* feat: ui menu item (#29266)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-22 20:56:00 -05:00
shenlong d4102c0489 refactor: ui icon buttons implicit loading (#29263)
* refactor: icon buttons implicit loading

* chore: cleanup

* feat: ui color override (#29264)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-22 16:24:52 -04:00
shenlong 30a73c1105 feat: mobile-ui snackbar (#29260)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-06-22 16:22:26 -04:00
Jason Rasmussen ec7c0f9ec8 fix: sync backfill (#29267) 2026-06-22 16:19:01 -04:00
Brandon Wees a5198e23a8 refactor: use SemVer classes for version compatability message (#29056)
* refactor: use SemVer classes for version compatability message

* chore: readd major version compatabilty messages

* fix: remove 1.106.0 check

(we dont support v1 servers anymore)
2026-06-22 11:28:56 -04:00
Mees Frensel 51f2905fcc fix(web): remove map's fullscreen button (#29192) 2026-06-22 16:58:07 +02:00
Vogeluff 3b7d75c18a feat(web): Add text-white-shadow to elements and increase the shadows effect (#29165)
* fix(web): increase text shadow strength for white text on thumbnails

* fix(web): fix class order for text-white-shadow

* fixup: format fix
2026-06-22 09:43:15 -05:00
Daniel Dietzler c484bd99b6 fix: ignore external libraries for integrity report checksum check (#29248) 2026-06-22 13:56:24 +00:00
Anthony Clerici c0bf5a4c56 fix(server): use VBR for QSV so the max bitrate is respected (#29240)
* fix(server): use VBR for QSV so the max bitrate is respected

* update test
2026-06-22 09:56:20 -04:00
MuySup d9d50d2848 fix: turkish readme translation (#29234)
* Translation completed

3-2-1 rule translated

* Fix formatting of warning message in Turkish README
2026-06-22 09:55:58 -04:00
Daniel Dietzler c7453a67fd fix: detail panel people reactivity and iterator consumption (#29250) 2026-06-22 15:47:09 +02:00
Daniel Dietzler e918e3a313 feat: keyboard seeking for new video player (#29208) 2026-06-22 09:42:59 -04:00
Matthew Momjian dc7d57ff9a fix(docsc): v3 bump (#29246)
v3 bump in docs
2026-06-21 20:44:58 -05:00
223 changed files with 4201 additions and 25345 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'zulu'
java-version: '17'
+2 -1
View File
@@ -25,11 +25,12 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@3530478ec30f84adedbfeb28f0d9527a290f50a9 # v0.0.57
uses: oasdiff/oasdiff-action/breaking@e24529087d93f837b28b50bb66ba9016380a7fcc # v0.1.2
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
fail-on: ERR
review: false
check-mobile-patches:
runs-on: ubuntu-latest
+2 -2
View File
@@ -406,7 +406,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -483,7 +483,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+1 -1
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
image: prom/prometheus@sha256:a75c5a35bc21d7afe69551eefa3cb1e1fb1775fe759408007a66b54ec3de1f29
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
+1 -1
View File
@@ -10,7 +10,7 @@ DB_DATA_LOCATION=./postgres
# TZ=Etc/UTC
# The Immich version to use. You can pin this to a specific version like "v2.1.0"
IMMICH_VERSION=v2
IMMICH_VERSION=v3
# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
+1 -1
View File
@@ -19,7 +19,7 @@ If this does not work, try running `docker compose up -d --force-recreate`.
| Variable | Description | Default | Containers |
| :----------------- | :------------------------------ | :-----: | :----------------------- |
| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning |
| `IMMICH_VERSION` | Image tags | `v3` | server, machine learning |
| `UPLOAD_LOCATION` | Host path for uploads | | server |
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
+1 -1
View File
@@ -29,7 +29,7 @@ docker image prune
## Versioning Policy
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`.
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`.
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
Switching back to an earlier version, even within the same minor release tag, is not supported.
+35 -4
View File
@@ -1,9 +1,9 @@
{
"about": "حول",
"account": "حساب",
"account": "الحساب",
"account_settings": "إعدادات الحساب",
"acknowledge": "أُدرك ذلك",
"action": "إجراء",
"action": "الإجراء",
"action_common_update": "تحديث",
"action_description": "مجموعة إجراءات لتنفيذها على المحتويات المصفاة",
"actions": "عمليات",
@@ -189,13 +189,25 @@
"machine_learning_smart_search_enabled": "تفعيل البحث الذكي",
"machine_learning_smart_search_enabled_description": "إذا تم تعطيله، فلن يتم ترميز الصور للبحث الذكي.",
"machine_learning_url_description": "عنوان URL لخادم التعلم الآلي. إذا تم توفير أكثر من عنوان URL واحد، سيتم محاولة الاتصال بكل خادم على حدة حتى يستجيب أحدهم بنجاح، بدءًا من الأول إلى الأخير. سيتم تجاهل الخوادم التي لا تستجيب مؤقتًا حتى تعود للعمل.",
"maintenance_backup_management": "إدارة النسخ الاحتياطي",
"maintenance_delete_backup": "حذف النسخ الاحتياطي",
"maintenance_delete_backup_description": "هذا الملف سيتم حذفه بشكل لا رجعه فيه.",
"maintenance_delete_error": "فشل حذف النسخ الاحتياطي.",
"maintenance_integrity_check": "تحقق",
"maintenance_integrity_check_all": "تحديد الكل",
"maintenance_integrity_checksum_mismatch": "عدم تطابق رمز التحقق",
"maintenance_integrity_checksum_mismatch_description": "الملفات التي لا يتطابق المجموع التدقيقي لها على القرص مع المجموع التدقيقي المخزن في قاعدة بيانات Immich.",
"maintenance_integrity_checksum_mismatch_job": "التحقق من عدم تطابق المجموع التدقيقي",
"maintenance_integrity_checksum_mismatch_refresh_job": "تحديث تقارير عدم تطابق المجموع التدقيقي",
"maintenance_integrity_missing_file": "الملفات المفقودة",
"maintenance_integrity_missing_file_description": "‌الملفات التي يتتبعها Immich في قاعدة بياناته ولكنها غير موجودة في نظام الملفات.",
"maintenance_integrity_missing_file_job": "التحقق من الملفات المفقودة",
"maintenance_integrity_missing_file_refresh_job": "تحديث تقارير الملفات المفقودة",
"maintenance_integrity_report": "تقرير السلامة",
"maintenance_integrity_untracked_file": "الملفات غير المتتبعة",
"maintenance_integrity_untracked_file_description": "الملفات الموجودة في مجلدات Immich بدون وجود سجلات لها.",
"maintenance_integrity_untracked_file_job": "التحقق من الملفات غير المتتبعة",
"maintenance_integrity_untracked_file_refresh_job": "تحديث تقارير الملفات غير المتتبعة",
"maintenance_restore_backup": "استعادة النسخ الاحتياطي",
"maintenance_restore_backup_description": "سيتم مسح بيانات Immich واستعادتها من النسخة الاحتياطي المختار. سيتم إنشاء نسخة احتياطية قبل المتابعة.",
"maintenance_restore_backup_different_version": "هذا النسخ الاحتياطي تم انشائه باستخدام اصدار مختلف من Immich!",
@@ -575,6 +587,7 @@
"asset_added_to_album": "تمت إضافته إلى الألبوم",
"asset_adding_to_album": "جارٍ الإضافة إلى الألبوم…",
"asset_created": "انشئ اصل",
"asset_day_count": "{date}: {count, plural, zero {لا توجد ملفات} one {ملف واحد} two {ملفان} few {# ملفات} many {# ملفًا} other {# ملف}}",
"asset_description_updated": "تم تحديث وصف المحتوى",
"asset_filename_is_offline": "الأصل {filename} غير متصل",
"asset_has_unassigned_faces": "يحتوي الأصل على وجوه غير مخصصة",
@@ -704,6 +717,7 @@
"backup_settings_subtitle": "إدارة إعدادات التحميل",
"backup_upload_details_page_more_details": "اضغط لتفاصيل اضافية",
"backward": "الى الوراء",
"battery_optimization_backup_reliability": "إيقاف تحسين البطارية يزيد من استقرار النسخ الاحتياطي في الخلفية",
"biometric_auth_enabled": "المصادقة البايومترية مفعله",
"biometric_locked_out": "لقد قفلت عنك المصادقة البيومترية",
"biometric_no_options": "لا توجد خيارات بايومترية متوفرة",
@@ -918,6 +932,8 @@
"deduplicate_all": "إلغاء تكرار الكل",
"default_locale": "الإعدادات المحلية الافتراضية",
"default_locale_description": "تنسيق التواريخ والأرقام بناءً على الإعدادات المحلية للمتصفح",
"default_quality_subtitle": "الجودة المستخدمة عند الضغط على \"مشاركة\". اضغط مطولاً على زر المشاركة لاختيار الجودة في كل مرة.",
"default_share_quality": "جودة المشاركة الافتراضية",
"delete": "حذف",
"delete_action_confirmation_message": "هل انت متأكد من حذف هذا الملف؟ هذا سؤدي الى نقل الملف الى سلة مهملات الخادم وسيتم اشعارك ان كنت تريد حذفه على الجهاز",
"delete_action_prompt": "تم حذف {count}",
@@ -1227,6 +1243,7 @@
"failed": "فشل",
"failed_count": "فشل: {count}",
"failed_to_authenticate": "فشل في المصادقة",
"failed_to_delete_file": "فشل حذف الملف",
"failed_to_load_assets": "فشل تحميل الأصول",
"failed_to_load_folder": "فشل تحميل المجلد",
"favorite": "مفضل",
@@ -1357,6 +1374,7 @@
"individual_share": "حصة فردية",
"individual_shares": "المشاركات الفردية",
"info": "معلومات",
"integrity_checks": "فحوصات السلامة",
"interval": {
"day_at_onepm": "كل يوم الساعة الواحدة ظهرا",
"hours": "كل {hours, plural, one {ساعة} other {{hours, number} ساعة}}",
@@ -1404,6 +1422,7 @@
"leave": "مغادرة",
"leave_album": "اترك الالبوم",
"lens_model": "نموذج العدسات",
"less": "أقل",
"let_others_respond": "دع الآخرين يستجيبون",
"level": "المستوى",
"library": "مكتبة",
@@ -1428,6 +1447,7 @@
"linked_oauth_account": "حساب مرتبط بـ OAuth",
"list": "قائمة",
"live": "حي",
"load_more": "تحميل المزيد",
"loading": "تحميل",
"loading_search_results_failed": "فشل تحميل نتائج البحث",
"local": "محلّي",
@@ -1528,7 +1548,6 @@
"map_location_picker_page_use_location": "استخدم هذا الموقع",
"map_location_service_disabled_content": "يجب تمكين خدمة الموقع لعرض الأصول من موقعك الحالي.هل تريد تمكينه الآن؟",
"map_location_service_disabled_title": "خدمة الموقع معطل",
"map_marker_for_images": "علامة الخريطة للصور الملتقطة في {city}، {country}",
"map_marker_with_image": "علامة الخريطة مع الصورة",
"map_no_location_permission_content": "هناك حاجة إلى إذن الموقع لعرض الأصول من موقعك الحالي.هل تريد السماح به الآن؟",
"map_no_location_permission_title": "تم رفض إذن الموقع",
@@ -1597,6 +1616,8 @@
"merge_people_prompt": "هل تريد دمج هؤلاء الناس؟ هذا الإجراء لا رجعة فيه.",
"merge_people_successfully": "تم دمج الأشخاص بنجاح",
"merged_people_count": "دمج {count, plural, one {شخص واحد} other {# أشخاص}}",
"minFaces": "الحد الأدنى للوجوه",
"minFaces_description": "الحد الأدنى لعدد الوجوه المتعرف عليها لكي يتم عرض الشخص",
"minimize": "تصغير",
"minute": "دقيقة",
"minutes": "دقائق",
@@ -1692,6 +1713,7 @@
"not_selected": "لم يختار",
"notes": "ملاحظات",
"nothing_here_yet": "لا يوجد شيء هنا بعد",
"notification_backup_reliability": "فعل الإشعارات للزيادة من استقرار النسخ الاحتياطي في الخلفية",
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
"notification_permission_list_tile_content": "منح إذن لتمكين الإخطارات.",
"notification_permission_list_tile_enable_button": "تمكين الإخطارات",
@@ -2083,6 +2105,7 @@
"select_person": "اختر شخص",
"select_person_to_tag": "اختر شخص لوضع علامة",
"select_photos": "تحديد الصور",
"select_quality": "تحديد الدقة",
"select_trash_all": "تحديد حذف الكلِ",
"select_user_for_sharing_page_err_album": "فشل في إنشاء ألبوم",
"selected": "التحديد",
@@ -2146,6 +2169,8 @@
"share_assets_selected": "اختيار {count}",
"share_dialog_preparing": "تحضير...",
"share_link": "مشاركة رابط",
"share_original": "استخدام الملف الأصلي",
"share_preview": "استخدام الصورة المصغرة",
"shared": "مُشتَرك",
"shared_album_activities_input_disable": "التعليق معطل",
"shared_album_activity_remove_content": "هل تريد حذف هذا النشاط؟",
@@ -2247,6 +2272,7 @@
"slideshow_repeat_description": "العودة إلى البداية عند انتهاء عرض الشرائح",
"slideshow_settings": "إعدادات عرض الشرائح",
"smart_album": "ألبوم ذكي",
"some_assets_already_have_a_location_warning": "بعض الملفات المحددة تحتوي بالفعل على موقع جغرافي",
"sort_albums_by": "رتب الألبومات حسب...",
"sort_created": "تاريخ الإنشاء",
"sort_items": "عدد العناصر",
@@ -2367,11 +2393,13 @@
"trash_page_title": "سلة المهملات ({count})",
"trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
"trigger": "مفعِل",
"trigger_asset_metadata_extraction": "استخراج البيانات الوصفية للملفات",
"trigger_asset_metadata_extraction_description": "يتم تفعيله عند استخراج البيانات الوصفية (EXIF) للملف",
"trigger_asset_uploaded": "رفع الاصل",
"trigger_asset_uploaded_description": "يتم تفعيله عند تحميل أصل جديد",
"trigger_description": "حدث يبدأ سير العمل",
"trigger_person_recognized": "تم التعرف على شخص",
"trigger_person_recognized_description": "يتم تفعيله عند اكتشاف شخص",
"trigger_person_recognized_description": "يتم تفعيله عند التعرف على شخص",
"trigger_type": "نوع المفعل",
"troubleshoot": "استكشاف المشاكل",
"type": "النوع",
@@ -2413,6 +2441,7 @@
"updated_password": "تم تحديث كلمة المرور",
"upload": "رفع",
"upload_concurrency": "الرفع المتزامن",
"upload_day_count": "{date}: {count, plural, one {# رفع الملف} other {# رفع الملفات}}",
"upload_details": "تفاصيل الرفع",
"upload_dialog_info": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟",
"upload_dialog_title": "تحميل الأصول",
@@ -2428,6 +2457,8 @@
"upload_to_immich": "الرفع الىImmich ({count})",
"uploading": "جاري الرفع",
"uploading_media": "رفع الوسائط",
"uploads": "عمليات الرفع",
"uploads_count": "{count, plural, one {# رفع الملف} other {# رفع الملفات}}",
"url": "عنوان URL",
"usage": "الاستخدام",
"use_biometric": "استخدم البايومتري",
-1
View File
@@ -1520,7 +1520,6 @@
"map_location_picker_page_use_location": "Выкарыстаць гэта месцазнаходжанне",
"map_location_service_disabled_content": "Каб паказваць аб’екты з вашага бягучага месцазнаходжання, трэба ўключыць службу геалакацыі. Жадаеце ўключыць яе зараз?",
"map_location_service_disabled_title": "Служба геалакацыі адключана",
"map_marker_for_images": "Маркер на карце для відарысаў, зробленых у {city}, {country}",
"map_marker_with_image": "Маркер на карце з відарысам",
"map_no_location_permission_content": "Каб паказваць аб’екты з вашага бягучага месцазнаходжання, патрэбен дазвол на геалакацыю. Дазволіць зараз?",
"map_no_location_permission_title": "Адмоўлена ў дазволе на геалакацыю",
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Използвай това местоположение",
"map_location_service_disabled_content": "За да се показват обектите от текущото място, трябва да бъде включена услугата за местоположение. Искате ли да я включите сега?",
"map_location_service_disabled_title": "Услугата за местоположение е изключена",
"map_marker_for_images": "Маркери на картата за снимки направени в {city}, {country}",
"map_marker_for_image": "Маркер на картата за снимка, направена в {city}, {country}",
"map_marker_with_image": "Маркер на картата с изображение",
"map_no_location_permission_content": "За да се показват обектите от текущото място, трябва разрешение за определяне на местоположението. Искате ли да предоставите разрешение сега?",
"map_no_location_permission_title": "Отказан достъп до местоположение",
+4
View File
@@ -27,6 +27,7 @@
"add_partner": "অংশীদার যোগ করুন",
"add_path": "পাথ যুক্ত করুন",
"add_photos": "ছবি যুক্ত করুন",
"add_step": "ধাপ যোগ করুন",
"add_tag": "ট্যাগ যুক্ত করুন",
"add_to": "যুক্ত করুন…",
"add_to_album": "এলবাম এ যোগ করুন",
@@ -78,6 +79,7 @@
"cron_expression_description": "Cron ফরম্যাট ব্যবহার করে স্ক্যানিং ইন্টারভ্যাল নির্ধারণ করুন। আরও তথ্যের জন্য দয়া করে <link>Crontab Guru</link> দেখুন",
"cron_expression_presets": "Cron এক্সপ্রেশন প্রিসেট",
"disable_login": "লগইন অক্ষম করুন",
"download_csv": "CSV ডাউনলোড করুন",
"duplicate_detection_job_description": "সদৃশ ছবি শনাক্ত করতে অ্যাসেটগুলোর উপর মেশিন লার্নিং চালান। এটি Smart Search-এর উপর নির্ভর করে",
"exclusion_pattern_description": "এক্সক্লুশন প্যাটার্ন ব্যবহার করে লাইব্রেরি স্ক্যান করার সময় নির্দিষ্ট ফাইল ও ফোল্ডার উপেক্ষা করা যায়। এটি তখনই উপকারী যখন কিছু ফোল্ডারে এমন ফাইল থাকে যা আপনি ইমপোর্ট করতে চান না, যেমন RAW ফাইল।",
"export_config_as_json_description": "বর্তমান সিস্টেম কনফিগারেশনটিকে একটি JSON ফাইল হিসেবে ডাউনলোড করুন",
@@ -187,9 +189,11 @@
"machine_learning_smart_search_enabled": "স্মার্ট সার্চ সক্ষম করুন",
"machine_learning_smart_search_enabled_description": "নিষ্ক্রিয় থাকলে, স্মার্ট সার্চের জন্য ছবিগুলো এনকোড (encode) করা হবে না।",
"machine_learning_url_description": "মেশিন লার্নিং সার্ভারের URL। যদি একের বেশি URL প্রদান করা হয়, তবে একটি সফলভাবে সাড়া না দেওয়া পর্যন্ত প্রতিটি সার্ভারে এক এক করে চেষ্টা করা হবে (প্রথম থেকে শেষ ক্রমানুসারে)। যে সার্ভারগুলো সাড়া দেবে না, সেগুলো পুনরায় সচল হওয়া পর্যন্ত সাময়িকভাবে উপেক্ষা করা হবে।",
"maintenance_backup_management": "ব্যাকআপ ব্যবস্থাপনা",
"maintenance_delete_backup": "ব্যাকআপ (Backup)মুছুন",
"maintenance_delete_backup_description": "এই ফাইলটি চিরতরে মুছে ফেলা হবে।",
"maintenance_delete_error": "ব্যাকআপ মুছে ফেলতে ব্যর্থ হয়েছে।",
"maintenance_integrity_check": "যাচাই",
"maintenance_restore_backup": "ব্যাকআপ পুনরুদ্ধার(Restore) করুন",
"maintenance_restore_backup_description": "Immich মুছে ফেলা হবে এবং নির্বাচিত ব্যাকআপ থেকে পুনরুদ্ধার করা হবে। কার্যক্রম চালিয়ে যাওয়ার আগে একটি ব্যাকআপ তৈরি করা হবে।",
"maintenance_restore_backup_different_version": "এই ব্যাকআপটি Immich-এর একটি ভিন্ন সংস্করণের মাধ্যমে তৈরি করা হয়েছিল!",
+7 -2
View File
@@ -189,18 +189,23 @@
"machine_learning_smart_search_enabled": "Activa la cerca intel·ligent",
"machine_learning_smart_search_enabled_description": "Si està desactivada, les imatges no es codificaran per la cerca intel·ligent.",
"machine_learning_url_description": "L'URL del servidor d'aprenentatge automàtic. Si es proporciona més d'un URL, s'intentarà accedir a cada servidor en ordre fins que un d'ells respongui correctament.",
"maintenance_backup_management": "Gestió de còpies de seguretat",
"maintenance_delete_backup": "Elimina la còpia de seguretat",
"maintenance_delete_backup_description": "Aquest fitxer s'eliminarà de forma permanent.",
"maintenance_delete_error": "No s'ha pogut suprimir la còpia de seguretat.",
"maintenance_integrity_check": "Verificació",
"maintenance_integrity_check_all": "Verificar tot",
"maintenance_integrity_checksum_mismatch": "Checksum incorrecte",
"maintenance_integrity_checksum_mismatch_description": "Fitxers els quals la suma de verificació al disc no coincideix amb la que Immich té emmagatzemada a la base de dades.",
"maintenance_integrity_checksum_mismatch_job": "Comprovar checksums",
"maintenance_integrity_checksum_mismatch_refresh_job": "Actualitzar errors de checksums",
"maintenance_integrity_missing_file": "Manquen fitxers",
"maintenance_integrity_missing_file_description": "Fitxers que l'Immich té registrats a la seva base de dades però que no existeixen al sistema de fitxers.",
"maintenance_integrity_missing_file_job": "Verificar fitxers que falten",
"maintenance_integrity_missing_file_refresh_job": "Refrescar informe de fitxers desapareguts",
"maintenance_integrity_report": "Informe Integritat",
"maintenance_integrity_untracked_file": "Arxius no rastrejats",
"maintenance_integrity_untracked_file_description": "Fitxers presents als directoris d'Immich que Immich no en té cap registre.",
"maintenance_integrity_untracked_file_job": "Consulta de fitxers no rastrejats",
"maintenance_integrity_untracked_file_refresh_job": "Actualitza els informes de fitxers no rastrejats",
"maintenance_restore_backup": "Restaura la còpia de seguretat",
@@ -483,7 +488,7 @@
"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 particulars [EXPERIMENTAL]",
"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_subtitle": "Activa el mode de només lectura, en què les fotos només es poden veure. Accions com seleccionar múltiples imatges, compartir, enviar a un dispositiu o eliminar queden desactivades. Activa o desactiva el mode de només lectura des de lavatar dusuari de la pantalla principal",
"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 [EXPERIMENTAL]",
@@ -1543,7 +1548,7 @@
"map_location_picker_page_use_location": "Utilitzar aquesta ubicació",
"map_location_service_disabled_content": "El servei de localització s'ha d'activar per mostrar els elements de la teva ubicació actual. Vols activar-lo ara?",
"map_location_service_disabled_title": "Servei de localització desactivat",
"map_marker_for_images": "Marcador de mapa per a imatges fetes a {city}, {country}",
"map_marker_for_image": "Marcador de mapa per a imatge obtinguda a {city}, {country}",
"map_marker_with_image": "Marcador de mapa amb imatge",
"map_no_location_permission_content": "Es necessita el permís de localització per mostrar els elements de la teva ubicació actual. Vols permetre-ho ara?",
"map_no_location_permission_title": "Permís de localització denegat",
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Použít tuto polohu",
"map_location_service_disabled_content": "Pro zobrazení fotek z vaší aktuální polohy musí být povolena služba určování polohy. Chcete ji nyní povolit?",
"map_location_service_disabled_title": "Služba určování polohy je zakázána",
"map_marker_for_images": "Značka na mapě pro snímky pořízené v {city}, {country}",
"map_marker_for_image": "Značka na mapě pro obrázek pořízený ve městě {city}, {country}",
"map_marker_with_image": "Značka mapy s obrázkem",
"map_no_location_permission_content": "Oprávnění polohy je nutné pro zobrazení fotek z vaší aktuální polohy. Chcete oprávnění nyní povolit?",
"map_no_location_permission_title": "Oprávnění polohy zamítnuto",
-1
View File
@@ -70,7 +70,6 @@
"feature_photo_updated": "Уйрӑм сӑнӳкерчӗк ҫӗнетнӗ",
"manage_sharing_with_partners": "Партнерсемпе пайланассине йӗркелесе пырӑр",
"map": "Карттӑ",
"map_marker_for_images": "{city}, {country} ҫинче ӳкернӗ ӳкерчӗксем валли карттӑ маркерӗ",
"map_marker_with_image": "Карттӑ маркерӗ ӳкерчӗкпе",
"map_settings": "Карттӑ ĕнерленĕвĕ",
"no_explore_results_message": "Хӑвӑр коллекципе киленмешкӗн сӑнӳкерчӗксем ытларах тийӗр.",
-1
View File
@@ -1526,7 +1526,6 @@
"map_location_picker_page_use_location": "Brug denne placering",
"map_location_service_disabled_content": "Placeringstjenesten skal aktiveres for at vise elementer fra din nuværende placering. Vil du aktivere den nu?",
"map_location_service_disabled_title": "Placeringstjenesten er deaktiveret",
"map_marker_for_images": "Kortmarkør for billeder taget i {city}, {country}",
"map_marker_with_image": "Kortmarkør med billede",
"map_no_location_permission_content": "Der kræves tilladelse til placeringen for at vise elementer fra din nuværende placering. Vil du give tilladelse?",
"map_no_location_permission_title": "Placeringstilladelse blev afvist",
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Diesen Standort verwenden",
"map_location_service_disabled_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste jetzt aktivieren?",
"map_location_service_disabled_title": "Ortungsdienste deaktiviert",
"map_marker_for_images": "Kartenmarkierung für Bilder, die in {city}, {country} aufgenommen wurden",
"map_marker_for_image": "Kartenmarkierung für in {city}, {country} aufgenommenes Bild",
"map_marker_with_image": "Kartenmarkierung mit Bild",
"map_no_location_permission_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste jetzt aktivieren?",
"map_no_location_permission_title": "Kein Zugriff auf den Standort",
+1
View File
@@ -79,6 +79,7 @@
"cron_expression_description": "Setze das Scanintervall im Cron-Format. Für mehr Informationen, siehe z. B. <link>Crontab Guru</link>",
"cron_expression_presets": "Vorlagen für Cron-Ausdrücke",
"disable_login": "Login deaktivierä",
"download_csv": "CSV herunterladen",
"duplicate_detection_job_description": "Verwendet maschinelles Lernen auf den Dateien, um Duplikate zu finden. Baut auf der intelligenten Suche auf",
"exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen deiner Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.",
"export_config_as_json_description": "Aktuelle Systemkonfiguration als JSON-Datei herunterladen",
-1
View File
@@ -1495,7 +1495,6 @@
"map_location_picker_page_use_location": "Χρησιμοποιήστε αυτήν την τοποθεσία",
"map_location_service_disabled_content": "Η υπηρεσία τοποθεσίας πρέπει να είναι ενεργοποιημένη για την εμφάνιση στοιχείων από την τρέχουσα τοποθεσία σας. Θέλετε να το ενεργοποιήσετε τώρα;",
"map_location_service_disabled_title": "Η υπηρεσία τοποθεσίας απενεργοποιήθηκε",
"map_marker_for_images": "Δείκτης χάρτη για εικόνες που τραβήχτηκαν σε {city}, {country}",
"map_marker_with_image": "Χάρτης δείκτη με εικόνα",
"map_no_location_permission_content": "Απαιτείται άδεια τοποθεσίας για την εμφάνιση στοιχείων από την τρέχουσα τοποθεσία σας. Θέλετε να το επιτρέψετε τώρα;",
"map_no_location_permission_title": "Η άδεια τοποθεσίας απορρίφθηκε",
+5 -317
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Use this location",
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
"map_location_service_disabled_title": "Location Service disabled",
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
"map_marker_for_image": "Map marker for image taken in {city}, {country}",
"map_marker_with_image": "Map marker with image",
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_no_location_permission_title": "Location Permission denied",
+99 -2
View File
@@ -199,6 +199,15 @@
"maintenance_integrity_checksum_mismatch_description": "Dosieroj pri kiuj la kontrolsumo stokita sur disko ne kongruas kun tiu en la datumbazo de Immich.",
"maintenance_integrity_checksum_mismatch_job": "Kontroli pri nekongruaj kontrolsumoj",
"maintenance_integrity_checksum_mismatch_refresh_job": "Refreŝigi la raporton pri nekongruaj kontrolsumoj",
"maintenance_integrity_missing_file": "Dosieroj mankantaj",
"maintenance_integrity_missing_file_description": "Dosieroj menciitaj en la datumbazo de Immich, sed kiuj ne (plu) ekzistas en la dosiersistemo.",
"maintenance_integrity_missing_file_job": "Detekti mankantajn dosierojn",
"maintenance_integrity_missing_file_refresh_job": "Refreŝigi raporton pri mankantaj dosieroj",
"maintenance_integrity_report": "Raporto pri integreco",
"maintenance_integrity_untracked_file": "Senspuraj dosieroj",
"maintenance_integrity_untracked_file_description": "Dosieroj en la dosierujoj de Immich, sed pri kiuj Immich havas neniun spuron.",
"maintenance_integrity_untracked_file_job": "Detekti senspurajn dosierojn",
"maintenance_integrity_untracked_file_refresh_job": "Refreŝigi raporton pri senspuraj dosieroj",
"maintenance_restore_backup": "Restaŭri savkopion",
"maintenance_restore_backup_description": "Immich estos forigita kaj reinstalita de la elektita savkopio. Nova savkopio estos kreita antaŭe.",
"maintenance_restore_backup_different_version": "Tiu ĉi savkopio estis kreita per alia versio de Immich!",
@@ -740,7 +749,7 @@
"cache_settings_title": "Agordoj pri kaŝmemoro",
"camera": "Fotilo",
"camera_brand": "Fabrikanto de fotilo",
"camera_model": "Modelo de fotilo",
"camera_model": "Tipo de fotilo",
"cancel": "Nuligi",
"cancel_search": "Nuligi serĉon",
"canceled": "Nuligita",
@@ -1454,6 +1463,7 @@
"log_out_all_devices": "Elsalutigi ĉiujn aparatojn",
"logged_in_as": "Ensalutita kiel {user}",
"logged_out_all_devices": "Ĉiuj aparatoj elsalutigitaj",
"logged_out_device": "Elsalutigita aparato",
"login": "Ensaluti",
"login_disabled": "Ensalutado malebligita",
"login_form_api_exception": "Eraro de API. Bonvolu kontroli la URL-on de la servilo, kaj reprovi.",
@@ -1529,7 +1539,6 @@
"map_location_picker_page_use_location": "Uzi tiun ĉi lokon",
"map_location_service_disabled_content": "Vi devas ŝalti la lokadan servon de via aparato por vidi elementojn de via aktuala loko. Ĉu vi volas nun ŝalti tion?",
"map_location_service_disabled_title": "Servo de lokado malŝaltita",
"map_marker_for_images": "Map-markilo por fotoj faritaj en {city}, {country}",
"map_marker_with_image": "Map-markilo kun bildo",
"map_no_location_permission_content": "Vi devas permesi al la apo detekti vian aktualan lokon por vidi tieajn elementojn. Ĉu vi volas nun permesi tion?",
"map_no_location_permission_title": "Detektado de loko ne permesita",
@@ -1638,6 +1647,9 @@
"navigate_to_time": "Navigi al dato/horo",
"network_requirement_photos_upload": "Uzi datumojn de poŝtelefona reto por savkopii fotojn",
"network_requirement_videos_upload": "Uzi datumojn de poŝtelefona reto por savkopii videojn",
"network_requirements": "Retaj postuloj",
"network_requirements_updated": "Retaj postuloj ŝanĝiĝis, savkopia vico restarigita",
"networking_settings": "Retkonektoj",
"networking_subtitle": "Administri agordojn pri finpunktoj de la servilo",
"never": "Neniam",
"new_album": "Nova albumo",
@@ -1888,6 +1900,13 @@
"readonly_mode_disabled": "Nurlega reĝimo malŝaltita",
"readonly_mode_enabled": "Nurlega reĝimo ŝaltita",
"ready_for_upload": "Preta por alŝuto",
"reassign": "Reatribui",
"reassigned_assets_to_existing_person": "Reatribuis {count, plural, one {# elementon} other {# elementojn}} al {name, select, null {ekzistanta homo} other {{name}}}",
"reassigned_assets_to_new_person": "Reatribuis {count, plural, one {# elementon} other {# elementojn}} al nova homo",
"reassing_hint": "Reatribuis elektitajn elementojn al ekzistanta homo",
"recent": "Lastatempa(j)",
"recent_albums": "Lastatempaj albumoj",
"recent_searches": "Lastatempaj serĉoj",
"recently_added": "Lastatempe aldonita(j)",
"recently_added_page_title": "Lastatempe aldonita(j)",
"recently_taken": "Lastatempe fotita(j)",
@@ -1911,21 +1930,73 @@
"remove_assets_shared_link_confirmation": "Ĉu vi certas, ke vi volas forigi {count, plural, one {# elementon} other {# elementojn}} de tiu dividita ligilo?",
"remove_assets_title": "Ĉu forigi elementojn?",
"remove_custom_date_range": "Forigi la dat-intervalon",
"remove_filter": "Forigi filtron",
"remove_from_album": "Forpreni de albumo",
"remove_from_album_action_prompt": "{count} forprenitaj de la albumo",
"remove_from_favorites": "Forigi el preferataĵoj",
"remove_from_lock_folder_action_prompt": "{count} forprenitaj de la ŝlosita dosierujo",
"remove_from_locked_folder": "Forpreni de la ŝlosita dosierujo",
"remove_from_locked_folder_confirmation": "Ĉu vi certas, ke vi volas forpreni tiujn fotojn/videojn el la ŝlosita dosierujo? Ili poste estos videblaj en via biblioteko.",
"remove_from_shared_link": "Forigi el dividita ligilo",
"remove_memory": "Forigi memoraĵon",
"remove_photo_from_memory": "Forpreni foton el tiu memoraĵo",
"remove_tag": "Forigi etikedon",
"remove_url": "Forigi URL-on",
"remove_user": "Forigi uzanton",
"removed_api_key": "Forigita API-ŝlosilo: {name}",
"removed_from_archive": "Forigita de la arĥivo",
"removed_from_favorites": "Forigita(j) el preferataĵoj",
"removed_from_favorites_count": "{count, plural, other {Forigis #}} el Preferataĵoj",
"removed_memory": "Memoraĵo forigita",
"removed_photo_from_memory": "Forprenis foton de la memoraĵo",
"removed_tagged_assets": "Forigis etikedon de {count, plural, one {# elemento} other {# elementoj}}",
"rename": "Renomi",
"repair": "Ripari",
"repair_no_results_message": "Senspuraj kaj mankantaj dosieroj aperas ĉi tie",
"replace_with_upload": "Anstataŭigi per alŝutaĵo",
"repository": "Deponejo",
"require_password": "Postuli pasvorton",
"require_user_to_change_password_on_first_login": "Devigi al uzanto ŝanĝi pasvorton je unua ensaluto",
"rescan": "Reanalizi",
"reset": "Restartigi",
"reset_password": "Restarigi pasvorton",
"reset_people_visibility": "Restarigi videblecon de homoj",
"reset_pin_code": "Restarigi PIN-kodon",
"reset_pin_code_description": "Se vi forgesis vian PIN-kodon, vi povas kontakti la administranto de via servilo por restarigi ĝin",
"reset_pin_code_success": "Sukcese restarigis PIN-kodon",
"reset_pin_code_with_password": "Vi povas restarigi vian PIN-kodon pere de via pasvorto",
"reset_sqlite": "Restarigi la SQLite-datumbazon",
"reset_sqlite_clear_app_data": "Forviŝi datumojn",
"reset_sqlite_confirmation": "Ĉu vi certas, ke vi volas forviŝi la datumojn de la apo? Tio forigos ĉiujn agordojn kaj elsalutigos vin.",
"reset_sqlite_confirmation_note": "Noto: vi devos relanĉi la apon por la forviŝo.",
"reset_sqlite_done": "Datumoj de la apo estas forviŝitaj. Bonvolu relanĉi Immich kaj ensalutu denove.",
"reset_sqlite_success": "Sukcese restarigis la SQLite-datumbazon",
"reset_to_default": "Restarigi la defaŭltojn",
"resolution": "Distingivo",
"resolve_duplicates": "Solvi duoblaĵojn",
"resolved_all_duplicates": "Solvis ĉiujn duoblaĵojn",
"restore": "Restaŭri",
"restore_all": "Restaŭri ĉiujn",
"restore_trash_action_prompt": "{count} restaŭrita(j) el rubujo",
"restore_user": "Restaŭri uzanton",
"restored_asset": "Restaŭri elementon",
"resume": "Daŭrigi",
"resume_paused_jobs": "Daŭrigi {count, plural, one {# paŭzitan taskon} other {# paŭzitajn taskojn}}",
"retry_upload": "Reprovi alŝuton",
"review_duplicates": "Kontroli duoblaĵojn",
"review_large_files": "Kontroli grandajn dosierojn",
"role": "Rolo",
"role_editor": "Redaktanto",
"role_viewer": "Spektanto",
"running": "Aktuale plenumata(j)",
"save": "Konservi",
"save_to_gallery": "Konservi en galerio",
"saved": "Konservita(j)",
"saved_api_key": "Konservis API-ŝlosilon",
"saved_profile": "Konservis profilon",
"saved_settings": "Konservis agordojn",
"say_something": "Skribu ion",
"scaffold_body_error_occurred": "Eraro okazis",
"scaffold_body_error_unrecoverable": "Neriparebla eraro okazis. Bonvolu sendi al ni la eraron kaj la stakspuron per Discord aŭ per Github por ke ni povu helpi. Vi povas forviŝi la ĉi-subajn datumojn de la apo se vi volas.",
"scan": "Analizi",
"scan_all_libraries": "Analizi ĉiujn bibliotekojn",
@@ -1933,7 +2004,33 @@
"scan_settings": "Agordoj pri analizado",
"scanning": "Analizado",
"scanning_for_album": "Serĉado de albumo...",
"screencast_mode_description": "Montri indikilojn surekrane pri tuŝoj de klavaro kaj muso",
"screencast_mode_title": "Baskuligi reĝimon de elsendo",
"search": "Serĉi",
"search_albums": "Serĉi albumojn",
"search_by_context": "Serĉi laŭ kunteksto",
"search_by_description": "Serĉi laŭ priskribo",
"search_by_description_example": "Promenado en Poznań",
"search_by_filename": "Serĉi laŭ dosiernomo aŭ sufikso",
"search_by_filename_example": "ekz. IMG_1234.jpg aŭ png",
"search_by_full_path": "Serĉi laŭ dosier-vojo aŭ dosierujo",
"search_by_full_path_example": "/Silvja/Projektoj/3D_Printado/2026-07-01 - vi povas serĉi 'Projektoj', '3D', 'Printado', '2026', ktp.",
"search_by_ocr": "Serĉi per optika signo-rekono",
"search_by_ocr_example": "Invitilo",
"search_camera_lens_model": "Serĉi tipon de objektivo...",
"search_camera_make": "Serĉi fabrikanton de fotilo...",
"search_camera_model": "Serĉi tipon de fotilo...",
"search_city": "Serĉi urbon...",
"search_country": "Serĉi landon...",
"search_filter_apply": "Apliki filtrilon",
"search_filter_camera_title": "Elektu tipon de fotilo",
"search_filter_date": "Dato",
"search_filter_date_interval": "de {start} ĝis {end}",
"search_filter_date_title": "Elektu intervalon de datoj",
"search_filter_display_option_not_in_album": "Ne en albumo",
"search_filter_display_options": "Agordoj pri aranĝo sur ekrano",
"search_filter_filename": "Serĉi laŭ dosiernomo",
"search_filter_location": "Loko",
"search_suggestion_list_smart_search_hint_1": "Inteligenta serĉado defaŭlte estas ŝaltita. Por serĉi metadatumojn, uzu sintakson tiel ",
"select_user_for_sharing_page_err_album": "Malsukcesis krei albumon",
"server_privacy": "Privateco de servilo",
+2 -2
View File
@@ -59,7 +59,7 @@
"backup_onboarding_1_description": "Copia en un lugar externo, en la nube u otra ubicación física.",
"backup_onboarding_2_description": "copias locales en diferentes dispositivos. Incluye los archivos principales y una copia de seguridad local de dichos archivos.",
"backup_onboarding_3_description": "copias totales de tu data, incluyendo los archivos originales. Incluye 1 copia fuera de sitio y 2 copias locales.",
"backup_onboarding_description": "Se recomienda una <backblaze-link>estrategia de copia de seguridad 3-2-1</backblaze-link> para proteger tus datos. Deberías mantener copias de las fotos y vídeos que subas, así como de la base de datos de Immich, para contar con una solución de copia de seguridad completa.",
"backup_onboarding_description": "Una <backblaze-link>estrategia de copia de seguridad 3-2-1</backblaze-link> es recomendada para proteger tus datos. Deberías mantener copias de las fotos y vídeos que subas, así como de la base de datos de Immich, para contar con una solución de copia de seguridad completa.",
"backup_onboarding_footer": "Para obtener más información sobre cómo hacer una copia de seguridad de Immich, consulta la <link>documentación</link>.",
"backup_onboarding_parts_title": "Una copia de seguridad 3-2-1 incluye:",
"backup_onboarding_title": "Copias de seguridad",
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Usar esta ubicación",
"map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar recursos de tu ubicación actual. ¿Deseas activarlos ahora?",
"map_location_service_disabled_title": "Servicios de ubicación desactivados",
"map_marker_for_images": "Marcador de mapa para imágenes tomadas en {city}, {country}",
"map_marker_for_image": "Marcador del mapa para la imagen tomada en {city}, {country}",
"map_marker_with_image": "Marcador de mapa con imagen",
"map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar recursos de tu ubicación actual. ¿Deseas activarlos ahora?",
"map_no_location_permission_title": "Permisos de ubicación denegados",
+11 -1
View File
@@ -189,11 +189,17 @@
"machine_learning_smart_search_enabled": "Luba nutiotsing",
"machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.",
"machine_learning_url_description": "Masinõppe serveri URL. Kui ette on antud rohkem kui üks URL, proovitakse neid järjest ükshaaval, kuni üks edukalt vastab. Servereid, mis ei vasta, ignoreeritakse ajutiselt, kuni ühendus taastub.",
"maintenance_backup_management": "Varunduse haldus",
"maintenance_delete_backup": "Kustuta varukoopia",
"maintenance_delete_backup_description": "See fail kustutatakse jäädavalt.",
"maintenance_delete_error": "Varukoopia kustutamine ebaõnnestus.",
"maintenance_integrity_check_all": "Märgi kõik",
"maintenance_integrity_checksum_mismatch": "Kontrollsumma ebakõla",
"maintenance_integrity_checksum_mismatch_description": "Failid, mille kontrollsumma ei klapi sellega, mis on Immich'i andmebaasis.",
"maintenance_integrity_checksum_mismatch_job": "Otsi kontrollsumma ebakõlasid",
"maintenance_integrity_checksum_mismatch_refresh_job": "Värskenda kontrollsumma ebakõlade aruanne",
"maintenance_integrity_missing_file": "Puuduvad failid",
"maintenance_integrity_missing_file_description": "Failid, mida Immich jälgib andmebaasis, kuid mida ei eksisteeri failisüsteemis.",
"maintenance_integrity_missing_file_job": "Otsi puuduvaid faile",
"maintenance_integrity_missing_file_refresh_job": "Värskenda puuduvate failide aruanne",
"maintenance_integrity_untracked_file": "Mittejälgitavad failid",
@@ -923,6 +929,8 @@
"deduplicate_all": "Dedubleeri kõik",
"default_locale": "Vaikimisi lokaat",
"default_locale_description": "Vorminda kuupäevad ja arvud vastavalt brauseri lokaadile",
"default_quality_subtitle": "Kvaliteet, mida jagamisel kasutada. Hoia jagamise nuppu all, et iga kord valida.",
"default_share_quality": "Vaikimisi jagamise kvaliteet",
"delete": "Kustuta",
"delete_action_confirmation_message": "Kas oled kindel, et soovid selle üksuse kustutada? See toiming liigutab üksuse serveri prügikasti ja küsib, kas soovid selle lokaalselt kustutada",
"delete_action_prompt": "{count} kustutatud",
@@ -1535,7 +1543,7 @@
"map_location_picker_page_use_location": "Kasuta seda asukohta",
"map_location_service_disabled_content": "Praeguse asukoha üksuste kuvamiseks tuleb lubada asukoha teenus. Kas soovid seda praegu lubada?",
"map_location_service_disabled_title": "Asukoha teenus keelatud",
"map_marker_for_images": "Kaardimarker kohas {city}, {country} tehtud piltide jaoks",
"map_marker_for_image": "Kaardimarker pildile, mis on tehtud kohas {city}, {country}",
"map_marker_with_image": "Kaardimarker pildiga",
"map_no_location_permission_content": "Praeguse asukoha üksuste kuvamiseks on vaja asukoha luba. Kas soovid seda praegu lubada?",
"map_no_location_permission_title": "Asukoha luba keelatud",
@@ -2376,6 +2384,8 @@
"trash_page_title": "Prügikast ({count})",
"trashed_items_will_be_permanently_deleted_after": "Prügikasti tõstetud üksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.",
"trigger": "Päästik",
"trigger_asset_metadata_extraction": "Üksuste metaandmete eraldamine",
"trigger_asset_metadata_extraction_description": "Käivitub, kui üksusest eraldatakse EXIF metaandmed",
"trigger_asset_uploaded": "Üksuse üleslaadimine",
"trigger_asset_uploaded_description": "Käivitub uue üksuse üleslaadimisel",
"trigger_description": "Sündmus, mis käivitab töövoo",
+1231 -12
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -605,7 +605,6 @@
"map_location_picker_page_use_location": "استفاده از این موقعیت مکانی",
"map_location_service_disabled_content": "برای نمایش دارایی‌ها بر اساس موقعیت مکانی، نیاز به فعال‌سازی سرویس مکان‌یابی دارید. می‌خواهید همین حالا فعال شود؟",
"map_location_service_disabled_title": "سرویس مکان‌یابی غیرفعال است",
"map_marker_for_images": "نشانگر روی نقشه برای عکس‌های گرفته‌شده در {city}, {country}",
"map_marker_with_image": "علامت‌گذاری نقشه با عکس",
"map_no_location_permission_content": "برای نمایش عکس‌های اطرافتان، برنامه نیاز به دسترسی به موقعیت مکانی دارد. اجازه دسترسی می‌دهید؟",
"map_no_location_permission_title": "دسترسی به موقعیت شما فعال نیست",
-1
View File
@@ -1504,7 +1504,6 @@
"map_location_picker_page_use_location": "Käytä tätä sijaintia",
"map_location_service_disabled_content": "Paikannuspalvelun pitää olla kytkettynä päälle, jotta nykyisen sijaintisi kohteita voidaan näyttää. Haluatko kytkeä sen päälle nyt?",
"map_location_service_disabled_title": "Paikannuspalvelu pois päältä",
"map_marker_for_images": "Karttamarkerointi kuville, jotka on otettu kaupungissa {city}, maassa {country}",
"map_marker_with_image": "Karttamarkerointi kuvalla",
"map_no_location_permission_content": "Paikannuslupa tarvitaan, jotta nykyisen sijainnin kohteita voidaan näyttää. Haluatko sallia pääsyn sijaintiin?",
"map_no_location_permission_title": "Paikannuslupa estetty",
+8
View File
@@ -15,29 +15,36 @@
"add_a_location": "Dagdagan ng lugar",
"add_a_name": "Dagdagan ng pangalan",
"add_a_title": "Dagdagan ng pamagat",
"add_action": "Magdagdag ng aksyon",
"add_assets": "Dagdagan ng asset",
"add_birthday": "Maglagay ng kaarawan",
"add_endpoint": "Dagdagan ng dulo",
"add_exclusion_pattern": "Magdagdag ng exlusion pattern",
"add_location": "Magdagdag ng lugar",
"add_more_users": "Magdagdag ng mga user",
"add_partner": "Magdagdag ng kasangga",
"add_path": "Magdagdag ng path",
"add_photos": "Magdagdag ng litrato",
"add_step": "Magdagdag ng step",
"add_tag": "Magdagdag ng tag",
"add_to": "Idagdag sa…",
"add_to_album": "Idagdag sa album",
"add_to_album_bottom_sheet_added": "Naidagdag sa {album}",
"add_to_album_bottom_sheet_already_exists": "Nasa {album} na",
"add_to_album_bottom_sheet_some_local_assets": "May ilang mga local assets ang hindi maidagdag sa album",
"add_to_album_toggle": "Toggle selection para sa {album}",
"add_to_albums": "Idagdag sa mga album",
"add_to_albums_count": "Idagdag sa mga album ({count})",
"add_to_bottom_bar": "Idagdag sa",
"add_to_shared_album": "Idagdag sa shared album",
"add_upload_to_stack": "Magdagdag ng upload para ma-stack",
"add_url": "Magdagdag ng URL",
"added_to_archive": "Naidagdag sa archive",
"added_to_favorites": "Naidagdag sa mga paborito",
"added_to_favorites_count": "Naidagdag ang {count, number} sa mga paborito",
"admin": {
"add_exclusion_pattern_description": "Dagdagan ng pattern para maibukod. Supportado ang pag-tutugma gamit ang *, **, at ?. Para hindi maisama ang mga file sa direktoryo na may pangalang \"Raw\", gamitin ang \"**/Raw/**\". Para hindi maisama ang lahat ng mga file na nagtatapos sa \".tif\", gamitin ang \"**/*.tif\". Para hindi maisama ang isang tiyak na folder, gamitin ang \"/path/to/ignore/**\".",
"admin_user": "Admin User",
"asset_offline_description": "Ang external library asset na ito ay hindi na makikita sa disk at nailipat na sa basurahan. Kung ang file ay nailipat sa loob ng library, tignan ang iyong timeline para sa kaukulang asset. Para maibalik ang asset na ito, siguraduhin na ang file ay maa-access ng Immich at muling i-scan ang library.",
"authentication_settings": "Setting ng mga Pagkakakilanlan",
"authentication_settings_description": "Pamahalaan ang password, OAuth, and iba pang setting ng pagkakakilanlan",
@@ -68,6 +75,7 @@
"disable_login": "I-disable ang login",
"duplicate_detection_job_description": "Hanapin ang mga magkakatulad na imahe gamit ang machine learning. Umaasa sa Smart Search",
"exclusion_pattern_description": "Maaaring gamitin ang mga pattern na pangbukod para hindi pansinin ang ilang file o folder habang binabasa ang iyong library. Mainam itong solusyon para sa mga folder na may file na ayaw niyong ma-import, tulad ng mga RAW na file.",
"face_detection": "Face detection",
"force_delete_user_warning": "BABALA: Tatanggalin itong user at lahat ng asset nila, Hindi ito mababawi at ang kanilang files ay hindi na mababalik",
"image_format": "Format",
"note_cannot_be_changed_later": "TANDAAN: Hindi na ito pwede baguhin sa susunod!",
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Utiliser ma position",
"map_location_service_disabled_content": "Le service de localisation doit être activé pour afficher les médias de votre emplacement actuel. Souhaitez-vous l'activer maintenant?",
"map_location_service_disabled_title": "Service de localisation désactivé",
"map_marker_for_images": "Marqueur de carte pour les images prises à {city}, {country}",
"map_marker_for_image": "Marqueur de carte pour une image prise à {city}, {country}",
"map_marker_with_image": "Marqueur de carte avec image",
"map_no_location_permission_content": "L'autorisation de localisation est nécessaire pour afficher les médias de votre emplacement actuel. Souhaitez-vous l'autoriser maintenant?",
"map_no_location_permission_title": "Permission de localisation refusée",
+5 -1
View File
@@ -189,18 +189,23 @@
"machine_learning_smart_search_enabled": "Cumasaigh cuardach cliste",
"machine_learning_smart_search_enabled_description": "Mura bhfuil sé sin ar fáil, ní dhéanfar íomhánna a ionchódú le haghaidh cuardaigh chliste.",
"machine_learning_url_description": "URL an fhreastalaí foghlama meaisín. Má chuirtear níos mó ná URL amháin ar fáil, déanfar iarracht ar gach freastalaí ceann ag an am go dtí go bhfreagróidh ceann acu go rathúil, in ord ón gcéad cheann go dtí an ceann deireanach. Déanfar neamhaird shealadach ar fhreastalaithe nach bhfreagróidh go dtí go mbeidh siad ar líne arís.",
"maintenance_backup_management": "Bainistíocht chúltaca",
"maintenance_delete_backup": "Scrios Cúltaca",
"maintenance_delete_backup_description": "Scriosfar an comhad seo go neamh-inchúlghairthe.",
"maintenance_delete_error": "Theip ar an gcúltaca a scriosadh.",
"maintenance_integrity_check": "Seiceáil",
"maintenance_integrity_check_all": "Seiceáil Gach Rud",
"maintenance_integrity_checksum_mismatch": "Mí-chomhoiriúnacht suime seiceála",
"maintenance_integrity_checksum_mismatch_description": "Comhaid nach bhfuil an tsuim seiceála ar an diosca ag teacht leis an tsuim seiceála atá stóráilte ag Immich ina bhunachar sonraí.",
"maintenance_integrity_checksum_mismatch_job": "Seiceáil le haghaidh mí-oiriúnuithe suime seiceála",
"maintenance_integrity_checksum_mismatch_refresh_job": "Athnuachan tuairiscí mí-oiriúnachta suime seiceála",
"maintenance_integrity_missing_file": "Comhaid ar Iarraidh",
"maintenance_integrity_missing_file_description": "Comhaid atá rianaithe ag Immich ina bhunachar sonraí ach nach bhfuil ar fáil ar an gcóras comhad.",
"maintenance_integrity_missing_file_job": "Seiceáil le haghaidh comhaid atá ar iarraidh",
"maintenance_integrity_missing_file_refresh_job": "Athnuachan tuairiscí ar chomhaid atá ar iarraidh",
"maintenance_integrity_report": "Tuarascáil Ionracais",
"maintenance_integrity_untracked_file": "Comhaid Gan Rianú",
"maintenance_integrity_untracked_file_description": "Comhaid in eolairí Immich nach bhfuil aon taifead ag Immich orthu.",
"maintenance_integrity_untracked_file_job": "Seiceáil le haghaidh comhaid neamhrianaithe",
"maintenance_integrity_untracked_file_refresh_job": "Athnuachan tuarascálacha comhad neamhrianaithe",
"maintenance_restore_backup": "Athchóirigh Cúltaca",
@@ -1543,7 +1548,6 @@
"map_location_picker_page_use_location": "Úsáid an suíomh seo",
"map_location_service_disabled_content": "Ní mór seirbhís suímh a chumasú chun sócmhainní ó do shuíomh reatha a thaispeáint. Ar mhaith leat é a chumasú anois?",
"map_location_service_disabled_title": "Seirbhís Suímh díchumasaithe",
"map_marker_for_images": "Marcóir léarscáile le haghaidh íomhánna a tógadh i {city}, {country}",
"map_marker_with_image": "Marcóir léarscáile le híomhá",
"map_no_location_permission_content": "Tá cead suímh ag teastáil chun sócmhainní a thaispeáint ó do shuíomh reatha. Ar mhaith leat é a cheadú anois?",
"map_no_location_permission_title": "Cead Suímh diúltaithe",
-1
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "Usar esta localización",
"map_location_service_disabled_content": "O servizo de localización debe estar activado para mostrar activos da súa localización actual. Quere activalo agora?",
"map_location_service_disabled_title": "Servizo de localización deshabilitado",
"map_marker_for_images": "Marcador de mapa para imaxes tomadas en {city}, {country}",
"map_marker_with_image": "Marcador de mapa con imaxe",
"map_no_location_permission_content": "Necesítase permiso de localización para mostrar activos da súa localización actual. Quere permitilo agora?",
"map_no_location_permission_title": "Permiso de localización denegado",
+4 -2
View File
@@ -1391,7 +1391,6 @@
"map_location_picker_page_use_location": "Ufnahmeort verwände",
"map_location_service_disabled_content": "DOrtigsdienscht müend aktiviert si, um Inhält am aktuelle Standort aazeige z chönne. Wotsch dOrtigsdienscht jetzt aktiviere?",
"map_location_service_disabled_title": "Ortigsdienscht deaktiviert",
"map_marker_for_images": "Charte-Markierige für Bilder, wo i {city}, {country} ufgnoh worde sind",
"map_marker_with_image": "Charte-Markierig mit Bild",
"map_no_location_permission_content": "DOrtigsdienscht müend aktiviert si, um Inhält am aktuelle Standort aazeige z chönne. Wotsch dOrtigsdienscht jetzt aktiviere?",
"map_no_location_permission_title": "Kei Zuegriff uf dä Standort",
@@ -1528,5 +1527,8 @@
"on_this_device": "Uf däm Grät",
"onboarding": "Iistig",
"onboarding_locale_description": "Wähl dini bevorzugti Sprooch. Du chasch die au spöter i dine Iistellige ändere.",
"onboarding_privacy_description": "Diä folgende (optionali) Funktione hänged vo externä Diänscht ab und chönd jederziit i de Iistellige deaktiviärt wärde."
"onboarding_privacy_description": "Diä folgende (optionali) Funktione hänged vo externä Diänscht ab und chönd jederziit i de Iistellige deaktiviärt wärde.",
"upload_finished": "Ufelade beändet",
"users": "Benutzer",
"waiting": "Usstehend"
}
-1
View File
@@ -1498,7 +1498,6 @@
"map_location_picker_page_use_location": "השתמש במיקום הזה",
"map_location_service_disabled_content": "שירות המיקום צריך להיות מופעל כדי להציג תמונות מהמיקום הנוכחי שלך. האם ברצונך להפעיל אותו עכשיו?",
"map_location_service_disabled_title": "שירות מיקום מבוטל",
"map_marker_for_images": "סמן מפה לתמונות שצולמו ב{city}, {country}",
"map_marker_with_image": "סמן מפה עם תמונה",
"map_no_location_permission_content": "יש צורך בהרשאה למיקום כדי להציג תמונות מהמיקום הנוכחי שלך. האם ברצונך לאפשר זאת עכשיו?",
"map_no_location_permission_title": "הרשאה למיקום נדחתה",
-1
View File
@@ -1479,7 +1479,6 @@
"map_location_picker_page_use_location": "इस स्थान का उपयोग करें",
"map_location_service_disabled_content": "आपके वर्तमान स्थान की संपत्तियाँ प्रदर्शित करने के लिए स्थान सेवा सक्षम होनी चाहिए। क्या आप इसे अभी सक्षम करना चाहते हैं?",
"map_location_service_disabled_title": "स्थान सेवा अक्षम",
"map_marker_for_images": "{city}, {country} में ली गई छवियों के लिए मानचित्र मार्कर",
"map_marker_with_image": "छवि के साथ मानचित्र मार्कर",
"map_no_location_permission_content": "आपके वर्तमान स्थान से संपत्तियाँ प्रदर्शित करने के लिए स्थान अनुमति आवश्यक है। क्या आप इसे अभी अनुमति देना चाहते हैं?",
"map_no_location_permission_title": "स्थान की अनुमति अस्वीकृत",
-1
View File
@@ -1414,7 +1414,6 @@
"map_location_picker_page_use_location": "Koristi ovu lokaciju",
"map_location_service_disabled_content": "Usluga lokacije mora biti omogućena za prikaz stavki s vaše trenutne lokacije. Želite li je sada omogućiti?",
"map_location_service_disabled_title": "Usluga lokacije onemogućena",
"map_marker_for_images": "Oznaka karte za slike snimljene u {city}, {country}",
"map_marker_with_image": "Oznaka karte sa slikom",
"map_no_location_permission_content": "Potrebno je dopuštenje za lokaciju kako bi se prikazale stavke s vaše trenutne lokacije. Želite li ga sada omogućiti?",
"map_no_location_permission_title": "Dopuštenje za lokaciju odbijeno",
+2 -2
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Kiválasztott hely használata",
"map_location_service_disabled_content": "A helymeghatározás szolgáltatást engedélyezni kell a jelenlegi helyednél lévő elemek megjelenítéséhez. Szeretnéd most engedélyezni?",
"map_location_service_disabled_title": "Helymeghatározás szolgáltatás letiltva",
"map_marker_for_images": "{country}, {city} helyen készült képek térképjelölője",
"map_marker_for_image": "Térképjelölő a következő helyen készült képhez: {city}, {country}",
"map_marker_with_image": "Térképjelölő képpel",
"map_no_location_permission_content": "A helymeghatározást engedélyezni kell a jelenlegi helyednél lévő elemek megjelenítéséhez. Szeretnéd most engedélyezni?",
"map_no_location_permission_title": "Helymeghatározás letiltva",
@@ -2122,7 +2122,7 @@
"server_privacy": "Szerver biztonság",
"server_restarting_description": "Az oldal pillanatokon belül frissül.",
"server_restarting_title": "A szerver újraindul",
"server_stats": "Szerver statisztikák",
"server_stats": "Szerver statisztika",
"server_update_available": "Szerverfrissítés érhető el",
"server_version": "Szerver verzió",
"set": "Beállít",
-1
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "Gunakan lokasi ini",
"map_location_service_disabled_content": "Layanan lokasi perlu diaktifkan untuk menampilkan aset yang terletak di lokasi Anda saat ini. Ingin mengaktifkan layanan tersebut sekarang?",
"map_location_service_disabled_title": "Layanan Lokasi nonaktif",
"map_marker_for_images": "Penanda peta untuk gambar yang diambil di {city}, {country}",
"map_marker_with_image": "Penanda peta dengan gambar",
"map_no_location_permission_content": "Izin lokasi diperlukan untuk menampilkan aset yang terletak di lokasi Anda. Ingin mengizinkannya sekarang?",
"map_no_location_permission_title": "Izin Lokasi ditolak",
+1 -2
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "Usa questa posizione",
"map_location_service_disabled_content": "I servizi di geolocalizzazione devono essere attivati per poter visualizzare le risorse dalla tua posizione attuale. Vuoi attivarli adesso?",
"map_location_service_disabled_title": "Servizio Localizzazione disattivato",
"map_marker_for_images": "Indicatore mappa per le immagini scattate in {city}, {country}",
"map_marker_with_image": "Segnaposto con immagine",
"map_no_location_permission_content": "L'accesso alla posizione è necessario per visualizzare le risorse dalla tua posizione attuale. Vuoi consentirlo adesso?",
"map_no_location_permission_title": "Autorizzazione Posizione negata",
@@ -1603,7 +1602,7 @@
},
"media_type": "Tipo Media",
"memories": "Ricordi",
"memories_all_caught_up": "Tutto a posto",
"memories_all_caught_up": "Niente di nuovo",
"memories_check_back_tomorrow": "Torna domani per altri ricordi",
"memories_setting_description": "Gestisci cosa vedi nei tuoi ricordi",
"memories_start_over": "Ricomincia",
-1
View File
@@ -1532,7 +1532,6 @@
"map_location_picker_page_use_location": "この位置情報を使う",
"map_location_service_disabled_content": "現在地の項目を表示するには位置情報がオンである必要があります。有効化しますか?",
"map_location_service_disabled_title": "位置情報がオフです",
"map_marker_for_images": "{country} {city}で撮影された写真の地図マーカー",
"map_marker_with_image": "画像の地図マーカー",
"map_no_location_permission_content": "現在地の項目を表示するには位置情報へのアクセスが必要です。許可しますか?",
"map_no_location_permission_title": "位置情報へのアクセスが拒否されました",
-1
View File
@@ -1101,7 +1101,6 @@
"map": "ನಕ್ಷೆ",
"map_cannot_get_user_location": "ಬಳಕೆದಾರರ ಸ್ಥಳವನ್ನು ಪಡೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ",
"map_location_service_disabled_content": "ನಿಮ್ಮ ಪ್ರಸ್ತುತ ಸ್ಥಳದಿಂದ ಸ್ವತ್ತುಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಸ್ಥಳ ಸೇವೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವ ಅಗತ್ಯವಿದೆ. ನೀವು ಈಗ ಅದನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ಬಯಸುವಿರಾ?",
"map_marker_for_images": "{city}, {country} ದಲ್ಲಿ ತೆಗೆದ ಚಿತ್ರಗಳಿಗಾಗಿ ನಕ್ಷೆ ಮಾರ್ಕರ್",
"map_marker_with_image": "ಚಿತ್ರದೊಂದಿಗೆ ನಕ್ಷೆ ಮಾರ್ಕರ್",
"map_no_location_permission_content": "ನಿಮ್ಮ ಪ್ರಸ್ತುತ ಸ್ಥಳದಿಂದ ಸ್ವತ್ತುಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಸ್ಥಳ ಅನುಮತಿ ಅಗತ್ಯವಿದೆ. ನೀವು ಈಗ ಅದನ್ನು ಅನುಮತಿಸಲು ಬಯಸುವಿರಾ?",
"map_zoom_to_see_photos": "ಫೋಟೋಗಳನ್ನು ನೋಡಲು ಝೂಮ್ ಔಟ್ ಮಾಡಿ",
+9 -6
View File
@@ -56,9 +56,9 @@
"backup_database": "데이터베이스 덤프 생성",
"backup_database_enable_description": "데이터베이스 덤프 활성화",
"backup_keep_last_amount": "보관할 이전 덤프 수",
"backup_onboarding_1_description": "개는 클라우드 다른 물리적 위치에 보관합니다.",
"backup_onboarding_2_description": "개는 서로 다른 로컬 장치에 보관하고,",
"backup_onboarding_3_description": "개의 데이터 사본을 만듭니다.",
"backup_onboarding_1_description": "클라우드 또는 다른 물리적 위치에 오프사이트 사본을 보관합니다.",
"backup_onboarding_2_description": "여러 장치에 로컬 복사본이 있습니다. 여기에는 주요 파일과 해당 파일의 로컬 백업이 포함됩니다.",
"backup_onboarding_3_description": "원본 파일을 포함한 데이터의 모든 사본 수입니다. 여기에는 오프사이트 사본 1개와 로컬 사본 2개가 포함됩니다.",
"backup_onboarding_description": "데이터 보호를 위해 <backblaze-link>3-2-1 백업 전략</backblaze-link> 사용을 권장합니다. 백업에는 업로드한 사진 및 동영상뿐 아니라 Immich 데이터베이스도 포함되어야 합니다.",
"backup_onboarding_footer": "Immich 백업에 대한 자세한 내용은 <link>공식 문서</link>를 참조하세요.",
"backup_onboarding_parts_title": "3-2-1 백업이란:",
@@ -181,7 +181,7 @@
"machine_learning_ocr_min_recognition_score": "최소 인식 점수",
"machine_learning_ocr_min_score_recognition_description": "인식할 텍스트의 최소 신뢰도 점수를 0~1 범위에서 설정합니다. 값이 작을수록 더 많은 텍스트를 인식하지만 잘못 인식될 가능성도 높아집니다.",
"machine_learning_ocr_model": "OCR 모델",
"machine_learning_ocr_model_description": "서버 모델은 모바일 모델보다 정확지만, 처리 시간이 길어지고 메모리 사용량도 늘어납니다.",
"machine_learning_ocr_model_description": "서버 모델은 모바일 모델보다 정확도가 높지만 처리 시간이 더 오래 걸리고 메모리 사용량도 더 많습니다.",
"machine_learning_settings": "기계 학습 설정",
"machine_learning_settings_description": "기계 학습 시 사용할 모델과 세부 설정을 관리합니다.",
"machine_learning_smart_search": "스마트 검색",
@@ -196,13 +196,16 @@
"maintenance_integrity_check": "체크",
"maintenance_integrity_check_all": "전체선택",
"maintenance_integrity_checksum_mismatch": "체크섬 불일치",
"maintenance_integrity_checksum_mismatch_description": "디스크의 체크섬이 Immich 데이터베이스에 저장된 체크섬과 일치하지 않는 파일입니다.",
"maintenance_integrity_checksum_mismatch_job": "파일 무결성 검사",
"maintenance_integrity_checksum_mismatch_refresh_job": "무결성 오류 보고서 새로고침",
"maintenance_integrity_missing_file": "누락된 파일",
"maintenance_integrity_missing_file_description": "Immich가 데이터베이스에서 추적했지만 파일 시스템에는 존재하지 않는 파일입니다.",
"maintenance_integrity_missing_file_job": "누락된 파일 확인",
"maintenance_integrity_missing_file_refresh_job": "누락된 파일 보고서 새로고침",
"maintenance_integrity_report": "무결성 보고서",
"maintenance_integrity_untracked_file": "추적되지 않은 파일",
"maintenance_integrity_untracked_file_description": "Immich의 디렉터리에 있지만 Immich가 기록을 가지고 있지 않은 파일들.",
"maintenance_integrity_untracked_file_job": "추적되지 않은 파일 확인",
"maintenance_integrity_untracked_file_refresh_job": "추적되지 않은 파일 보고서 새로고침",
"maintenance_restore_backup": "백업 복원",
@@ -1545,7 +1548,7 @@
"map_location_picker_page_use_location": "이 위치 사용",
"map_location_service_disabled_content": "현재 위치의 항목을 표시하려면 위치 서비스를 활성화해야 합니다. 지금 활성화하시겠습니까?",
"map_location_service_disabled_title": "위치 서비스 비활성화됨",
"map_marker_for_images": "{country}, {city}에서 촬영 이미지의 지도 마커",
"map_marker_for_image": "{city}, {country}에서 촬영 이미지의 지도 마커입니다.",
"map_marker_with_image": "이미지가 있는 지도 마커",
"map_no_location_permission_content": "현재 위치의 항목을 표시하려면 위치 권한이 필요합니다. 지금 허용하시겠습니까?",
"map_no_location_permission_title": "위치 권한 거부됨",
@@ -1838,7 +1841,7 @@
"play_motion_photo": "모션 포토 재생",
"play_or_pause_video": "동영상 재생/일시 정지",
"play_original_video": "원본 동영상 재생",
"play_original_video_setting_description": "트랜스코딩된 영상보다 원본 영상을 우선 재생합니다. 원본이 호환되지 않는 형식인 경우 정상적으로 재생되지 않을 수 있습니다.",
"play_original_video_setting_description": "변환된 영상보다 원본 영상을 재생하는 것을 권장합니다. 원본 영상이 호환되지 않으면 제대로 재생되지 않을 수 있습니다.",
"play_transcoded_video": "트랜스코딩 동영상 재생",
"please_auth_to_access": "계속 진행하려면 인증하세요.",
"plugin_method_filter_type": "필터",
+7 -2
View File
@@ -189,18 +189,23 @@
"machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką",
"machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.",
"machine_learning_url_description": "Mašininio mokymosi serverio URL. Jei pateikta daugiau nei vienas URL, serveriai bus bandomi eilės tvarka nuo pirmo iki paskutinio tol, kol bus rastas vienas veikiantis serveris.",
"maintenance_backup_management": "Atsarginių kopijų tvarkymas",
"maintenance_delete_backup": "Ištrinti atsarginę kopiją",
"maintenance_delete_backup_description": "Šis failas bus negrįžtamai ištrintas.",
"maintenance_delete_error": "Nepavyko ištrinti atsarginės kopijos.",
"maintenance_integrity_check": "Tikrinti",
"maintenance_integrity_check_all": "Tikrinti Visus",
"maintenance_integrity_checksum_mismatch": "Checksum neatitikimas",
"maintenance_integrity_checksum_mismatch_description": "Failai, kurių kontrolinė suma diske nesutampa su Immich duomenų bazėje įrašyta kontroline suma.",
"maintenance_integrity_checksum_mismatch_job": "Tikrinti checksum neatitikimų",
"maintenance_integrity_checksum_mismatch_refresh_job": "Atnaujinti checksum neatitikimo ataskaitas",
"maintenance_integrity_missing_file": "Trūkstami failai",
"maintenance_integrity_missing_file_description": "Failai, įtraukti į Immich duomenų bazę, tačiau neegzistuojantys failų sistemoje.",
"maintenance_integrity_missing_file_job": "Tikrinti, ar nėra trūkstamų failų",
"maintenance_integrity_missing_file_refresh_job": "Atnaujinti trūkstamų failų ataskaitas",
"maintenance_integrity_report": "Vientisumo Ataskaita",
"maintenance_integrity_untracked_file": "Nesekami Failai",
"maintenance_integrity_untracked_file_description": "Failai Immich kataloguose, apie kuriuos Immich neturi jokių įrašų.",
"maintenance_integrity_untracked_file_job": "Patikrinti, ar nėra nesekamų failų",
"maintenance_integrity_untracked_file_refresh_job": "Atnaujinti nesekamų failų ataskaitas",
"maintenance_restore_backup": "Atstatyti atsarginę kopiją",
@@ -209,7 +214,7 @@
"maintenance_restore_backup_unknown_version": "Nepavyko nustatyti atsarginės kopijos versijos.",
"maintenance_restore_database_backup": "Atstatyti duomenų bazę",
"maintenance_restore_database_backup_description": "Grąžinti į ankstesnę duomenų bazės būseną naudojant atsarginę kopiją",
"maintenance_settings": "Aptarnavimas",
"maintenance_settings": "Priežiūra",
"maintenance_settings_description": "Perjungti „Immich“ į aptarnavimo režimą.",
"maintenance_start": "Perjungti į aptarnavimo režimą",
"maintenance_start_error": "Nepavyko paleisti aptarnavimo režimo.",
@@ -1543,7 +1548,7 @@
"map_location_picker_page_use_location": "Naudoti šią vietovę",
"map_location_service_disabled_content": "Vietovės servisas turi būti įjungtas, kad rodytų elementus iš dabartinės vietovės. Įjungti vietovės servisą?",
"map_location_service_disabled_title": "Vietovės servisas išjungtas",
"map_marker_for_images": "Žemėlapio žymeklis nuotraukoms yra {city}, {country}",
"map_marker_for_image": "Žemėlapio žymeklis nuotraukai, padarytai {city}, {country}",
"map_marker_with_image": "Žemėlapio žymeklis su nuotrauka",
"map_no_location_permission_content": "Reikalingas vietovės leidimas, kad rodytų elementus iš dabartinės vietovės. Ar norite suteikti leidimą?",
"map_no_location_permission_title": "Vietovės leidimas atmestas",
-1
View File
@@ -1498,7 +1498,6 @@
"map_location_picker_page_use_location": "Izvēlēties šo atrašanās vietu",
"map_location_service_disabled_content": "Lai tiktu rādīti jūsu pašreizējās atrašanās vietas faili, ir jāaktivizē atrašanās vietas pakalpojums. Vai vēlaties to iespējot tagad?",
"map_location_service_disabled_title": "Atrašanās vietas Pakalpojums atslēgts",
"map_marker_for_images": "Kartes marķieris attēliem, kas uzņemti {city}, {country}",
"map_marker_with_image": "Kartes marķieris ar attēlu",
"map_no_location_permission_content": "Atrašanās vietas atļauja ir nepieciešama, lai parādītu jūsu pašreizējās atrašanās vietas aktīvus. Vai vēlaties to atļaut tagad?",
"map_no_location_permission_title": "Atrašanās vietas Atļaujas liegtas",
-1
View File
@@ -1346,7 +1346,6 @@
"map_location_picker_page_use_location": "ഈ സ്ഥലം ഉപയോഗിക്കുക",
"map_location_service_disabled_content": "നിങ്ങളുടെ നിലവിലെ സ്ഥാനത്ത് നിന്നുള്ള അസറ്റുകൾ പ്രദർശിപ്പിക്കുന്നതിന് ലൊക്കേഷൻ സേവനം പ്രവർത്തനക്ഷമമാക്കേണ്ടതുണ്ട്. ഇപ്പോൾ പ്രവർത്തനക്ഷമമാക്കണോ?",
"map_location_service_disabled_title": "ലൊക്കേഷൻ സേവനം പ്രവർത്തനരഹിതമാക്കി",
"map_marker_for_images": "{city}, {country} എന്നിവിടങ്ങളിൽ എടുത്ത ചിത്രങ്ങൾക്കുള്ള മാപ്പ് മാർക്കർ",
"map_marker_with_image": "ചിത്രത്തോടുകൂടിയ മാപ്പ് മാർക്കർ",
"map_no_location_permission_content": "നിങ്ങളുടെ നിലവിലെ സ്ഥാനത്ത് നിന്നുള്ള അസറ്റുകൾ പ്രദർശിപ്പിക്കുന്നതിന് ലൊക്കേഷൻ അനുമതി ആവശ്യമാണ്. ഇപ്പോൾ അനുവദിക്കണോ?",
"map_no_location_permission_title": "ലൊക്കേഷൻ അനുമതി നിഷേധിച്ചു",
-1
View File
@@ -1341,7 +1341,6 @@
"map_location_picker_page_use_location": "हे लोकेशन वापरा",
"map_location_service_disabled_content": "सध्याच्या लोकेशनवरील अॅसेट्स दाखवण्यासाठी लोकेशन सेवा सक्षम असणे आवश्यक आहे. तुम्हाला ती आत्ता सक्षम करायची आहे का?",
"map_location_service_disabled_title": "लोकेशन सेवा बंद आहे",
"map_marker_for_images": "{city}, {country} येथे घेतलेल्या प्रतिमांसाठी नकाशा मार्कर",
"map_marker_with_image": "प्रतिमेसह नकाशा मार्कर",
"map_no_location_permission_content": "सध्याच्या लोकेशनवरील अॅसेट्स दाखवण्यासाठी लोकेशन परवानगी आवश्यक आहे. तुम्हाला ती परवानगी आत्ता द्यायची आहे का?",
"map_no_location_permission_title": "लोकेशन परवानगी नाकारली",
+3 -4
View File
@@ -80,7 +80,7 @@
"cron_expression_presets": "Forhåndsinnstillinger for Cron-uttrykk",
"disable_login": "Deaktiver innlogging",
"download_csv": "Last ned CSV",
"duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av Smart Søk",
"duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av smartsøk",
"exclusion_pattern_description": "Ekskluderingsmønstre lar deg ignorere filer og mapper når du skanner biblioteket ditt. Dette er nyttig hvis du har mapper som inneholder filer du ikke vil importere, for eksempel RAW-filer.",
"export_config_as_json_description": "Last ned nåværende systemkonfigurasjon som en JSON fil",
"external_libraries_page_description": "Administrering for eksterne bibliotek",
@@ -187,7 +187,7 @@
"machine_learning_smart_search": "Smart søk",
"machine_learning_smart_search_description": "Søk etter bilder semantisk ved å bruke CLIP-embeddings",
"machine_learning_smart_search_enabled": "Aktiver smart søk",
"machine_learning_smart_search_enabled_description": "Hvis deaktivert, vil bilder ikke bli enkodet for smart søk.",
"machine_learning_smart_search_enabled_description": "Hvis deaktivert så blir ikke bilder kodet for smartsøk.",
"machine_learning_url_description": "URL til maskinlærings-serveren. Hvis mer enn en URL er lagt inn, hver server vill bli forsøkt en om gangen frem til en svarer suksessfullt, i rekkefølge fra først til sist. Servere som ikke svarer vil midlertidig bli oversett frem til dem svarer igjen.",
"maintenance_backup_management": "Administrasjon av sikkerhetskopier",
"maintenance_delete_backup": "Slett sikkerhetskopi",
@@ -205,7 +205,7 @@
"maintenance_integrity_missing_file_refresh_job": "Oppdater rapporten for manglende filer",
"maintenance_integrity_report": "Integritetsrapport",
"maintenance_integrity_untracked_file": "Usporede filer",
"maintenance_integrity_untracked_file_description": "Filer i Immich-mappene som ikke er registrert i databasen.",
"maintenance_integrity_untracked_file_description": "Filer i Immichs mapper som Immich ikke har noen oversikt over.",
"maintenance_integrity_untracked_file_job": "Sjekk etter usporede filer",
"maintenance_integrity_untracked_file_refresh_job": "Oppdater rapporten for usporede filer",
"maintenance_restore_backup": "Gjenopprett Sikkerhetskopi",
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "Bruk dette stedet",
"map_location_service_disabled_content": "Lokasjonstjeneste må være aktivert for å vise elementer fra din nåværende lokasjon. Vil du aktivere det nå?",
"map_location_service_disabled_title": "Lokasjonstjeneste deaktivert",
"map_marker_for_images": "Kart makeringer for bilder tatt i {city}, {country}",
"map_marker_with_image": "Kartmarkør med bilde",
"map_no_location_permission_content": "Lokasjonstilgang er påkrevet for å vise elementer fra din nåværende lokasjon. Vil du tillate det nå?",
"map_no_location_permission_title": "Lokasjonstilgang avvist",
+17 -1
View File
@@ -1 +1,17 @@
{}
{
"about": "बारे",
"account": "खाता",
"account_settings": "खाता सेटिङ",
"acknowledge": "स्वीकार",
"action": "कार्य",
"action_description": "छानियेको चिजमा सामुहिक कार्य",
"add_a_description": "थप विवरण",
"add_a_location": "स्थान थप्नुहोस्",
"add_a_name": "नाम हाल्नुहोस्",
"add_a_title": "शीर्षक हाल्नुहोस्",
"add_action": "कार्य थप्नुहोस्",
"add_action_description": "कार्य गर्नको लागि थप कार्यमा क्लिक गर्नुहोस्",
"add_assets": "फोटोहरू थप्नुहोस्",
"add_birthday": "जन्मदिन हाल्नुहोस",
"add_endpoint": "अन्तिम बिन्दु थप्नुहोस्"
}
+31 -31
View File
@@ -609,7 +609,7 @@
"asset_skipped": "Overgeslagen",
"asset_skipped_in_trash": "In prullenbak",
"asset_trashed": "Asset verwijderd",
"asset_troubleshoot": "Asset probleemoplossing",
"asset_troubleshoot": "Item probleemoplossing",
"asset_uploaded": "Geüpload",
"asset_uploading": "Uploaden…",
"asset_viewer_settings_subtitle": "Beheer je instellingen voor galerijweergave",
@@ -638,14 +638,14 @@
"assets_were_part_of_album_count": "{count, plural, one {Item was} other {Items waren}} al onderdeel van het album",
"assets_were_part_of_albums_count": "{count, plural, one {Item is} other {Items zijn}} al onderdeel van de albums",
"authorized_devices": "Geautoriseerde apparaten",
"automatic_endpoint_switching_subtitle": "Maak indien beschikbaar lokaal verbinding via het aangewezen wifi-netwerk en gebruik elders alternatieve verbindingen",
"automatic_endpoint_switching_subtitle": "Maak indien beschikbaar lokaal verbinding via het aangewezen wifinetwerk en gebruik elders alternatieve verbindingen",
"automatic_endpoint_switching_title": "Automatische serverwissel",
"autoplay_slideshow": "Diavoorstelling automatisch afspelen",
"back": "Terug",
"back_close_deselect": "Terug, sluiten of deselecteren",
"background_backup_running_error": "Back-up draait op de achtergrond, handmatige back-up kan niet worden gestart",
"background_location_permission": "Achtergrond locatie toestemming",
"background_location_permission_content": "Om van netwerk te wisselen terwijl de app op de achtergrond draait, heeft Immich *altijd* toegang tot de exacte locatie nodig om de naam van het WiFi-netwerk te kunnen lezen",
"background_location_permission_content": "Om van netwerk te wisselen terwijl de app op de achtergrond draait, heeft Immich *altijd* toegang tot de exacte locatie nodig om de naam van het wifi­netwerk te kunnen lezen",
"background_options": "Achtergrond opties",
"backup": "Back-up",
"backup_album_selection_page_albums_device": "Albums op apparaat ({count})",
@@ -680,7 +680,7 @@
"backup_controller_page_background_is_on": "Automatische achtergrond back-up staat aan",
"backup_controller_page_background_turn_off": "Achtergrondservice uitzetten",
"backup_controller_page_background_turn_on": "Achtergrondservice aanzetten",
"backup_controller_page_background_wifi": "Alleen op WiFi",
"backup_controller_page_background_wifi": "Alleen op wifi",
"backup_controller_page_backup": "Back-up",
"backup_controller_page_backup_selected": "Geselecteerd: ",
"backup_controller_page_backup_sub": "Geback-upte foto's en video's",
@@ -785,7 +785,7 @@
"charging_requirement_mobile_backup": "Achtergrond backup vereist dat het apparaat wordt opgeladen",
"check_corrupt_asset_backup": "Controleer op corrupte back-ups van items",
"check_corrupt_asset_backup_button": "Controle uitvoeren",
"check_corrupt_asset_backup_description": "Voer deze controle alleen uit via WiFi en nadat alle items zijn geback-upt. De procedure kan een paar minuten duren.",
"check_corrupt_asset_backup_description": "Voer deze controle alleen uit via wifi en nadat van alle items een back-up gemaakt is. De procedure kan een paar minuten duren.",
"check_logs": "Controleer logboek",
"checksum": "Controlegetal",
"choose": "Kies",
@@ -795,14 +795,14 @@
"cleanup_confirm_prompt_title": "Van dit apparaat verwijderen?",
"cleanup_deleted_assets": "{count} items verplaats naar prullenbak van apparaat",
"cleanup_deleting": "Naar prullenbak verplaatsen...",
"cleanup_found_assets": "Er zijn {count} backup bestanden gevonden",
"cleanup_found_assets_with_size": "Er zijn {count} back-upbestanden gevonden ({size})",
"cleanup_found_assets": "Er zijn {count} back-up­bestanden gevonden",
"cleanup_found_assets_with_size": "Er zijn {count} back-up­bestanden gevonden ({size})",
"cleanup_icloud_shared_albums_excluded": "Gedeelde albums van iCloud zijn uitgesloten van de scan",
"cleanup_no_assets_found": "Er zijn geen bestanden gevonden die aan bovenstaande criteria voldoen. Free Up Space kan alleen bestanden verwijderen die op de server zijn geback-upt",
"cleanup_preview_title": "Bestanden te verwijderen ({count})",
"cleanup_step3_description": "Scan naar back-upbestanden die overeenkomen met uw datum en behoud uw instellingen.",
"cleanup_step4_summary": "{count} bestanden (gemaakt vóór {date}) die van uw lokale apparaat moeten worden verwijderd. Foto's blijven toegankelijk via de Immich-app.",
"cleanup_trash_hint": "Om de opslagruimte volledig vrij te maken, opent u de systeemgalerij-app en leegt u de prullenbak",
"cleanup_trash_hint": "Open de galerij-app en leeg de prullenbak om de opslagruimte volledig vrij te maken",
"clear": "Wissen",
"clear_all": "Alles wissen",
"clear_all_recent_searches": "Wis alle recente zoekopdrachten",
@@ -814,7 +814,7 @@
"client_cert_enter_password": "Voer wachtwoord in",
"client_cert_import": "Importeren",
"client_cert_import_success_msg": "Cliëntcertificaat is geïmporteerd",
"client_cert_invalid_msg": "Ongeldig certificaatbestand of verkeerd wachtwoord",
"client_cert_invalid_msg": "Ongeldig certificaat­bestand of verkeerd wachtwoord",
"client_cert_password_message": "Voer het wachtwoord voor dit certificaat in",
"client_cert_password_title": "Certificaat wachtwoord",
"client_cert_remove_msg": "Clientcertificaat is verwijderd",
@@ -850,7 +850,7 @@
"confirm_tag_face_unnamed": "Wil je dit gezicht taggen?",
"connected_device": "Verbonden apparaat",
"connected_to": "Verbonden met",
"contain": "Bevat",
"contain": "Passend",
"context": "Context",
"continue": "Doorgaan",
"control_bottom_app_bar_add_tags": "Tags toevoegen",
@@ -1066,7 +1066,7 @@
"enabled": "Ingeschakeld",
"end_date": "Einddatum",
"enqueued": "In de wachtrij",
"enter_wifi_name": "Voer de WiFi-naam in",
"enter_wifi_name": "Voer de naam van het wifinetwerk in",
"enter_your_pin_code": "Voer uw pincode in",
"enter_your_pin_code_subtitle": "Voer uw pincode in om toegang te krijgen tot de vergrendelde map",
"error": "Fout",
@@ -1114,7 +1114,7 @@
"failed_to_stack_assets": "Fout bij stapelen van items",
"failed_to_tag_assets": "Fout bij taggen van items",
"failed_to_unstack_assets": "Fout bij ontstapelen van items",
"failed_to_update_notification_status": "Kon notificatiestatus niet updaten",
"failed_to_update_notification_status": "Kan notificatie­status niet updaten",
"incorrect_email_or_password": "Onjuist e-mailadres of wachtwoord",
"library_folder_already_exists": "Dit import­pad bestaat al.",
"page_not_found": "Pagina niet gevonden",
@@ -1237,7 +1237,7 @@
"external": "Extern",
"external_libraries": "Externe bibliotheken",
"external_network": "Extern netwerk",
"external_network_sheet_info": "Als je niet verbonden bent met het opgegeven WiFi-netwerk, maakt de app verbinding met de server via de eerst bereikbare URL in de onderstaande lijst, van boven naar beneden",
"external_network_sheet_info": "Als je niet verbonden bent met het opgegeven wifinetwerk, maakt de app verbinding met de server via de eerst bereikbare URL in de onderstaande lijst, van boven naar beneden",
"f_number": "Diafragma",
"face_unassigned": "Niet toegewezen",
"failed": "Mislukt",
@@ -1261,7 +1261,7 @@
"filename": "Bestandsnaam",
"filetype": "Bestandstype",
"filter": "Filter",
"filter_description": "Filtervoorwaarden voor doel items",
"filter_description": "Filter­voorwaarden voor betreffende items",
"filter_people": "Filteren op persoon",
"filter_places": "Filteren op locatie",
"filter_tags": "Filteren op label",
@@ -1273,7 +1273,7 @@
"folder": "Map",
"folder_not_found": "Map niet gevonden",
"folders": "Mappen",
"folders_feature_description": "Bladeren door de mapweergave van de foto's en video's op het bestandssysteem",
"folders_feature_description": "Bladeren door de map­weergave van de foto's en video's op het bestands­systeem",
"forgot_pin_code_question": "Pincode vergeten?",
"forward": "Vooruit",
"free_up_space": "Maak opslag vrij",
@@ -1287,7 +1287,7 @@
"geolocation_instruction_location": "Klik op een item met gps-coördinaten om de locatie te gebruiken, of kies een locatie direct op de kaart",
"get_help": "Hulp vragen",
"get_people_error": "Fout bij ophalen mensen",
"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",
"get_wifiname_error": "Kon de naam van het netwerk niet ophalen. Zorg ervoor dat je de benodigde machtig­ingen hebt verleend en verbonden bent met een wifinetwerk",
"getting_started": "Aan de slag",
"go_back": "Ga terug",
"go_to_folder": "Ga naar map",
@@ -1387,9 +1387,9 @@
"invite_to_album": "Uitnodigen voor album",
"ios_debug_info_fetch_ran_at": "Ophalen gelukt op {dateTime}",
"ios_debug_info_last_sync_at": "Laatst gesynchroniseerd {dateTime}",
"ios_debug_info_no_processes_queued": "Geen achtergrondprocessen in de wachtrij",
"ios_debug_info_no_processes_queued": "Geen achtergrond­processen in de wachtrij",
"ios_debug_info_no_sync_yet": "Er is nog geen achtergrondsynchronisatie uitgevoerd",
"ios_debug_info_processes_queued": "{count, plural, one {{count} achtergrondproces in de wachtrij} other {{count} achtergrondprocessen in de wachtrij}}",
"ios_debug_info_processes_queued": "{count, plural, one {{count} achtergrond­proces} other {{count} achtergrond­processen}} in de wachtrij",
"ios_debug_info_processing_ran_at": "Verwerking uitgevoerd op {dateTime}",
"iso": "ISO",
"items_count": "{count, plural, one {# item} other {# items}}",
@@ -1456,10 +1456,10 @@
"local_id": "Lokaal ID",
"local_media_summary": "Lokale media samenvatting",
"local_network": "Lokaal netwerk",
"local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven WiFi-netwerk wordt gebruikt",
"local_network_sheet_info": "De app maakt verbinding met de server via deze URL wanneer het opgegeven wifinetwerk wordt gebruikt",
"location": "Locatie",
"location_permission": "Locatietoestemming",
"location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige WiFi-netwerk te kunnen bepalen",
"location_permission_content": "Om de functie voor automatische serverwissel te gebruiken, heeft Immich toegang tot de exacte locatie nodig om de naam van het huidige wifinetwerk te kunnen bepalen",
"location_picker_choose_on_map": "Kies op kaart",
"location_picker_latitude_error": "Voer een geldige breedtegraad in",
"location_picker_latitude_hint": "Voer hier je breedtegraad in",
@@ -1502,8 +1502,8 @@
"longitude": "Lengtegraad",
"look": "Uiterlijk",
"loop_videos": "Video's herhalen",
"loop_videos_description": "Inschakelen om video's automatisch te herhalen in de detailweergave.",
"main_branch_warning": "Je gebruikt een ontwikkelingsversie. We raden je ten zeerste aan een releaseversie te gebruiken!",
"loop_videos_description": "Inschakelen om video's automatisch te herhalen in de detail­weergave.",
"main_branch_warning": "Je gebruikt een ontwikkelings­versie. We raden je ten zeerste aan een release­versie te gebruiken!",
"main_menu": "Hoofdmenu",
"maintenance_action_restore": "Database wordt hersteld",
"maintenance_description": "Immich is in de <link>onderhouds­modus</link> gezet.",
@@ -1512,7 +1512,7 @@
"maintenance_logged_in_as": "Momenteel ingelogd als {user}",
"maintenance_restore_from_backup": "Herstellen vanaf backup",
"maintenance_restore_library": "Bibliotheek herstellen",
"maintenance_restore_library_confirm": "Als dit er goed uit ziet ga dan verder om de backup terug te zetten!",
"maintenance_restore_library_confirm": "Als dit er goed uit ziet, ga dan verder om de back-up terug te zetten!",
"maintenance_restore_library_description": "Database wordt hersteld",
"maintenance_restore_library_folder_has_files": "{folder} heeft {count} map(pen)",
"maintenance_restore_library_folder_no_files": "{folder} mist bestanden!",
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Gebruik deze locatie",
"map_location_service_disabled_content": "Locatie service moet ingeschakeld zijn om items van je huidige locatie weer te geven. Wil je het nu inschakelen?",
"map_location_service_disabled_title": "Locatie service uitgeschakeld",
"map_marker_for_images": "Kaartmarkering voor afbeeldingen gemaakt in {city}, {country}",
"map_marker_for_image": "Kaartmarkering voor afbeelding gemaakt in {city}, {country}",
"map_marker_with_image": "Kaartmarkering met afbeelding",
"map_no_location_permission_content": "Locatietoestemming is nodig om items van je huidige locatie weer te geven. Wil je dit nu toestaan?",
"map_no_location_permission_title": "Locatietoestemming geweigerd",
@@ -1983,7 +1983,7 @@
"reset_sqlite": "SQLite database resetten",
"reset_sqlite_clear_app_data": "Wis gegevens",
"reset_sqlite_confirmation": "Weet je zeker dat je de app-gegevens wilt wissen? Hiermee worden alle instellingen verwijderd en word je uitgelogd.",
"reset_sqlite_confirmation_note": "Let op: Je moet de app opnieuw opstarten nadat je deze hebt gewist.",
"reset_sqlite_confirmation_note": "Let op: je moet de app opnieuw opstarten nadat je deze hebt gewist.",
"reset_sqlite_done": "App data is gewist. Start Immich opnieuw op en log opnieuw in.",
"reset_sqlite_success": "De SQLite database is succesvol gereset",
"reset_to_default": "Resetten naar standaard",
@@ -1996,7 +1996,7 @@
"restore_user": "Gebruiker herstellen",
"restored_asset": "Item hersteld",
"resume": "Hervatten",
"resume_paused_jobs": "Hervat {count, plural, one {# gepauseerde taak} other {# gepauseerde taken}}",
"resume_paused_jobs": "{count, plural, one {# gepauzeerde taak} other {# gepauzeerde taken}} hervatten",
"retry_upload": "Opnieuw uploaden",
"review_duplicates": "Controleer duplicaten",
"review_large_files": "Grote bestanden beoordelen",
@@ -2470,8 +2470,8 @@
"use_template": "Gebruik template",
"user": "Gebruiker",
"user_has_been_deleted": "Deze gebruiker is verwijderd.",
"user_id": "Gebruikers ID",
"user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {} other {dit item}} geliket",
"user_id": "Gebruikers-ID",
"user_liked": "{user} vindt {type, select, photo {deze foto} video {deze video} asset {} other {dit item}} leuk",
"user_pin_code_settings": "Pincode",
"user_pin_code_settings_description": "Beheer je pincode",
"user_privacy": "Gebruikersprivacy",
@@ -2479,7 +2479,7 @@
"user_purchase_settings_description": "Beheer je aankoop",
"user_role_set": "{user} instellen als {role}",
"user_usage_detail": "Gedetailleerd gebruik van gebruikers",
"user_usage_stats": "Statistieken van accountgebruik",
"user_usage_stats": "Accountstatistieken",
"user_usage_stats_description": "Bekijk statistieken van accountgebruik",
"username": "Gebruikersnaam",
"users": "Gebruikers",
@@ -2532,7 +2532,7 @@
"welcome_to_immich": "Welkom bij Immich",
"when": "Wanneer",
"width": "Breedte",
"wifi_name": "WiFi-naam",
"wifi_name": "Wifinetwerk",
"workflow": "Werkstroom",
"workflow_delete_prompt": "Weet je zeker dat je deze werkstroom wilt verwijderen?",
"workflow_deleted": "Werkstroom verwijderd",
@@ -2554,7 +2554,7 @@
"years_ago": "{years, plural, one {Een jaar} other {# jaar}} geleden",
"yes": "Ja",
"you_dont_have_any_shared_links": "Je hebt geen gedeelde links",
"your_wifi_name": "Je WiFi-naam",
"your_wifi_name": "Je wifinetwerk",
"zero_to_clear_rating": "druk op 0 om de sterwaardering te verwijderen",
"zoom_image": "Inzoomen",
"zoom_to_bounds": "Zoom naar randen"
+3 -3
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Użyj tej lokalizacji",
"map_location_service_disabled_content": "Aby wyświetlić zasoby z Twojej bieżącej lokalizacji, należy włączyć usługę lokalizacyjną. Czy chcesz to teraz włączyć?",
"map_location_service_disabled_title": "Usługa lokalizacji wyłączona",
"map_marker_for_images": "Wskaźnik mapy dla zdjęć zrobionych w {city}, {country}",
"map_marker_for_image": "Znacznik na mapie dla zdjęcia wykonanego w {city}, {country}",
"map_marker_with_image": "Znacznik na mapie ze zdjęciem",
"map_no_location_permission_content": "Aby wyświetlić zasoby z Twojej bieżącej lokalizacji, potrzebne jest pozwolenie na lokalizację. Czy chcesz teraz na to pozwolić?",
"map_no_location_permission_title": "Odmowa dostępu do lokalizacji",
@@ -2031,7 +2031,7 @@
"search_by_full_path": "Wyszukaj według pełnej ścieżki lub folderu",
"search_by_full_path_example": "/John/Projekty/Drukowanie_3D/2026-07-01 możesz wyszukiwać hasła takie jak Projekty, 3D, Drukowanie, 2026 itp.",
"search_by_ocr": "Wyszukaj przy użyciu OCR",
"search_by_ocr_example": "Latte",
"search_by_ocr_example": "Kawa, trampolina",
"search_camera_lens_model": "Wyszukaj model obiektywu...",
"search_camera_make": "Wyszukaj markę aparatu...",
"search_camera_model": "Wyszukaj model aparatu...",
@@ -2185,7 +2185,7 @@
"shared_by_you": "Udostępnione przez ciebie",
"shared_from_partner": "Zdjęcia od {partner}",
"shared_intent_upload_button_progress_text": "{current} / {total} Przesłano",
"shared_link_app_bar_title": "Udostępnione",
"shared_link_app_bar_title": "Udostępnione linki",
"shared_link_clipboard_copied_massage": "Skopiowane do schowka",
"shared_link_clipboard_text": "Link: {link}\nHasło: {password}",
"shared_link_create_error": "Błąd podczas tworzenia linka do udostępnienia",
-1
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "Utilizar esta localização",
"map_location_service_disabled_content": "Serviço de localização precisa de estar ativado para mostrar recursos da localização atual. Deseja ativar agora?",
"map_location_service_disabled_title": "Serviço de localização desativado",
"map_marker_for_images": "Marcador no mapa para fotos tiradas em {city}, {country}",
"map_marker_with_image": "Marcador de mapa com imagem",
"map_no_location_permission_content": "A permissão da localização é necessária para mostrar recursos da localização atual. Deseja conceder a permissão agora?",
"map_no_location_permission_title": "Permissão de localização foi negada",
+6 -1
View File
@@ -189,18 +189,23 @@
"machine_learning_smart_search_enabled": "Habilitar a Pesquisa Inteligente",
"machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para pesquisa inteligente.",
"machine_learning_url_description": "A URL do servidor de aprendizado de máquina. Se mais de uma URL for fornecida, elas serão tentadas, uma de cada vez e na ordem indicada, até que uma responda com sucesso. Servidores que não responderem serão ignorados temporariamente até voltarem a estar conectados.",
"maintenance_backup_management": "Gerenciamento de backup",
"maintenance_delete_backup": "Excluir Backup",
"maintenance_delete_backup_description": "Este arquivo será excluído de forma irreversível.",
"maintenance_delete_error": "Falha ao excluir o backup.",
"maintenance_integrity_check": "Verificar",
"maintenance_integrity_check_all": "Verificar tudo",
"maintenance_integrity_checksum_mismatch": "Checksum não corresponde",
"maintenance_integrity_checksum_mismatch_description": "Arquivos cujo o checksum atual não corresponde ao checksum que Immich armazenou no banco de dados.",
"maintenance_integrity_checksum_mismatch_job": "Verificar se há erros de checksum",
"maintenance_integrity_checksum_mismatch_refresh_job": "Atualizar # de checksum sem correspondência",
"maintenance_integrity_missing_file": "Arquivos não encontrados",
"maintenance_integrity_missing_file_description": "Arquivos que Immich rastreou em seu banco de dados, mas que não existem no sistema de arquivos.",
"maintenance_integrity_missing_file_job": "Verificar se há arquivos não encontrados",
"maintenance_integrity_missing_file_refresh_job": "Atualizar # de arquivos não encontrados",
"maintenance_integrity_report": "Relatório de integridade",
"maintenance_integrity_untracked_file": "Arquivos não rastreados",
"maintenance_integrity_untracked_file_description": "Arquivos não rastreados dentro dos diretórios do Immich.",
"maintenance_integrity_untracked_file_job": "Verificar se há arquivos não rastreados",
"maintenance_integrity_untracked_file_refresh_job": "Atualizar # de arquivos não rastreados",
"maintenance_restore_backup": "Restaurar Backup",
@@ -1543,7 +1548,7 @@
"map_location_picker_page_use_location": "Use esta localização",
"map_location_service_disabled_content": "O serviço de localização precisa estar ativado para exibir os arquivos da sua localização atual. Deseja ativar agora?",
"map_location_service_disabled_title": "Serviço de localização desativado",
"map_marker_for_images": "Marcador de mapa para imagens tiradas em {city}, {country}",
"map_marker_for_image": "Marcador do mapa para a foto tirada em {city}, {country}",
"map_marker_with_image": "Marcador de mapa com imagem",
"map_no_location_permission_content": "É necessária a permissão de localização para exibir os arquivos da sua localização atual. Deseja conceder a permissão agora?",
"map_no_location_permission_title": "Permissão de localização foi negada",
-1
View File
@@ -1523,7 +1523,6 @@
"map_location_picker_page_use_location": "Folosește această locație",
"map_location_service_disabled_content": "Serviciul de localizare trebuie să fie activat pentru a afișa resursele din locația actuală. Dorești să o activezi acum?",
"map_location_service_disabled_title": "Serviciul de localizare este dezactivat",
"map_marker_for_images": "Marcator de hartă pentru imaginile realizate în {city}, {country}",
"map_marker_with_image": "Marcator de hartă cu imagine",
"map_no_location_permission_content": "Permisiunea de localizare este necesară pentru a afișa resursele din locația actuală. Dorești să o activezi acum?",
"map_no_location_permission_title": "Permisiunea de localizare este dezactivată",
+2 -2
View File
@@ -1548,8 +1548,8 @@
"map_location_picker_page_use_location": "Это местоположение",
"map_location_service_disabled_content": "Для отображения объектов в текущем месте необходимо включить службу определения местоположения. Включить?",
"map_location_service_disabled_title": "Служба определения местоположения отключена",
"map_marker_for_images": "Маркер на карте для изображений, сделанных в {city}, {country}",
"map_marker_with_image": "Маркер на карте с изображением",
"map_marker_for_image": "Маркер на карте для объекта, сделанного в {city}, {country}",
"map_marker_with_image": "Маркер на карте для объекта",
"map_no_location_permission_content": "Для отображения объектов в текущем месте необходимо разрешение на определение местоположения. Предоставить разрешение?",
"map_no_location_permission_title": "Доступ к местоположению отклонен",
"map_settings": "Настройки карты",
-1
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "Použiť túto polohu",
"map_location_service_disabled_content": "Služba určovania polohy musí byť povolená, aby sa zobrazovali položky z vašej aktuálnej polohy. Chcete ju teraz zapnúť?",
"map_location_service_disabled_title": "Služba určovania polohy vypnutá",
"map_marker_for_images": "Značka na mape pre obrázky odfotené v {city}, {country}",
"map_marker_with_image": "Mapová značka pre obrázok",
"map_no_location_permission_content": "Na zobrazenie položiek z vašej aktuálnej polohy je potrebné povolenie na polohu. Chcete to teraz povoliť?",
"map_no_location_permission_title": "Povolenie polohy zamietnuté",
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Uporabi to lokacijo",
"map_location_service_disabled_content": "Lokacijska storitev mora biti omogočena za prikaz sredstev z vaše trenutne lokacije. Ali jo želite takoj omogočiti?",
"map_location_service_disabled_title": "Lokacijska storitev onemogočena",
"map_marker_for_images": "Oznaka zemljevida za slike, posnete v {city}, {country}",
"map_marker_for_image": "Oznaka na zemljevidu za sliko, posneto v {city}, {country}",
"map_marker_with_image": "Oznaka zemljevida s sliko",
"map_no_location_permission_content": "Za prikaz sredstev z vaše trenutne lokacije je potrebno dovoljenje za lokacijo. Ali to želite takoj dovoliti?",
"map_no_location_permission_title": "Dovoljenje za lokacijo je zavrnjeno",
-1
View File
@@ -1184,7 +1184,6 @@
"map_location_picker_page_use_location": "Користите ову локацију",
"map_location_service_disabled_content": "Услуга локације мора бити омогућена да би се приказивала средства са ваше тренутне локације. Да ли желите да је сада омогућите?",
"map_location_service_disabled_title": "Услуга локације је онемогућена",
"map_marker_for_images": "Означивач на мапи за слике снимљене у {city}, {country}",
"map_marker_with_image": "Маркер на мапи са сликом",
"map_no_location_permission_content": "Потребна је дозвола за локацију да би се приказали ресурси са ваше тренутне локације. Да ли желите да је сада дозволите?",
"map_no_location_permission_title": "Дозвола за локацију је одбијена",
-1
View File
@@ -1370,7 +1370,6 @@
"map_location_picker_page_use_location": "Koristite ovu lokaciju",
"map_location_service_disabled_content": "Usluga lokacije mora biti omogućena da bi se prikazivala sredstva sa vaše trenutne lokacije. Da li želite da je sada omogućite?",
"map_location_service_disabled_title": "Usluga lokacije je onemogućena",
"map_marker_for_images": "Označivač na mapi za slike snimljene u {city}, {country}",
"map_marker_with_image": "Marker na mapi sa slikom",
"map_no_location_permission_content": "Potrebna je dozvola za lokaciju da bi se prikazali resursi sa vaše trenutne lokacije. Da li želite da je sada dozvolite?",
"map_no_location_permission_title": "Dozvola za lokaciju je odbijena",
+1 -1
View File
@@ -1548,7 +1548,7 @@
"map_location_picker_page_use_location": "Använd den här platsen",
"map_location_service_disabled_content": "Platstjänst måste vara aktiverad för att visa objekt från din nuvarande plats. Vill du aktivera den nu?",
"map_location_service_disabled_title": "Platstjänst inaktiverad",
"map_marker_for_images": "Kartmarkering för bilder tagna i {city}, {country}",
"map_marker_for_image": "Kartmarkör för bild tagen i {city}, {country}",
"map_marker_with_image": "Kartmarkör med bild",
"map_no_location_permission_content": "Platsrättighet är nödvändigt för att kunna visa objekt från din nuvarande plats. Vill du tillåta det nu?",
"map_no_location_permission_title": "Platsrättighet nekad",
-1
View File
@@ -1485,7 +1485,6 @@
"map_location_picker_page_use_location": "இந்த இருப்பிடத்தைப் பயன்படுத்தவும்",
"map_location_service_disabled_content": "உங்கள் தற்போதைய இருப்பிடத்திலிருந்து சொத்துக்களைக் காட்ட இருப்பிட பணி இயக்கப்பட வேண்டும். இப்போது அதை இயக்க விரும்புகிறீர்களா?",
"map_location_service_disabled_title": "இருப்பிட பணி முடக்கப்பட்டது",
"map_marker_for_images": "{city}, {country}",
"map_marker_with_image": "படத்துடன் வரைபட மார்க்கர்",
"map_no_location_permission_content": "உங்கள் தற்போதைய இருப்பிடத்திலிருந்து சொத்துக்களைக் காட்ட இருப்பிட இசைவு தேவை. இப்போது அதை அனுமதிக்க விரும்புகிறீர்களா?",
"map_no_location_permission_title": "இருப்பிட இசைவு மறுக்கப்பட்டது",
-1
View File
@@ -837,7 +837,6 @@
"manage_your_devices": "మీ లాగిన్ అయిన పరికరాలను నిర్వహించండి",
"manage_your_oauth_connection": "మీ OAuth కనెక్షన్‌ని నిర్వహించండి",
"map": "మ్యాప్",
"map_marker_for_images": "{city}, {country} లో తీసిన చిత్రాల కోసం మ్యాప్ మార్కర్",
"map_marker_with_image": "చిత్రంతో మ్యాప్ మార్కర్",
"map_settings": "మ్యాప్ సెట్టింగ్‌లు",
"matches": "మ్యాచ్‌లు",
-1
View File
@@ -1468,7 +1468,6 @@
"map_location_picker_page_use_location": "ใช้ตำแหน่งนี้",
"map_location_service_disabled_content": "ต้องเปิดตำแหน่งเพื่อแสดงทรัพยากรจากตำแหน่งปัจจุบัน เปิดตอนนี้?",
"map_location_service_disabled_title": "บริการตำแหน่งถูกปิด",
"map_marker_for_images": "หมุดแผนที่สำหรับรูปถ่ายที่ {city}, {country}",
"map_marker_with_image": "หมุดแผนที่กับรูปถ่าย",
"map_no_location_permission_content": "จำเป็นต้องมีสิทธิ์เข้าถึงตำแหน่งเพื่อแสดงทรัพยากรจากตำแหน่งปัจจุบัน อนุญาตตอนนี้?",
"map_no_location_permission_title": "สิทธิ์เข้าถึงตำแหน่งถูกปฏิเสธ",
+61 -3
View File
@@ -79,6 +79,7 @@
"cron_expression_description": "Cron formatını kullanarak tarama aralığını belirle. Daha fazla bilgi için örneğin <link>Crontab Guru</link>ya bakın",
"cron_expression_presets": "Cron ifadesi ön ayarları",
"disable_login": "Girişi devre dışı bırak",
"download_csv": "CSVyi indir",
"duplicate_detection_job_description": "Benzer fotoğrafları bulmak için makine öğrenmesini çalıştır. Bu işlem Akıllı Arama'ya bağlıdır",
"exclusion_pattern_description": "Kütüphaneyi tararken dosya ve klasörleri görmezden gelmek için dışlama desenlerini kullanabilirsiniz. RAW dosyaları gibi bazı dosya ve klasörleri içe aktarmak istemediğinizde bu seçeneği kullanabilirsiniz.",
"export_config_as_json_description": "Geçerli sistem yapılandırmasını JSON dosyası olarak indir",
@@ -188,9 +189,23 @@
"machine_learning_smart_search_enabled": "Akıllı aramayı etkinleştir",
"machine_learning_smart_search_enabled_description": "Eğer devre dışı bırakılırsa fotoğraflar akıllı arama için işlenmeyecek.",
"machine_learning_url_description": "Makine öğrenimi sunucusunun URLsi. Birden fazla URL sağlanırsa, her sunucu sırayla tek tek denenir ve biri başarılı yanıt verene kadar devam edilir. Yanıt vermeyen sunucular, çevrimiçi duruma gelene kadar geçici olarak yok sayılır.",
"maintenance_backup_management": "Yedekleme sistemi",
"maintenance_delete_backup": "Yedeği Sil",
"maintenance_delete_backup_description": "Bu dosya geri alınamaz şekilde silinecektir.",
"maintenance_delete_error": "Yedek silinemedi.",
"maintenance_integrity_check": "Kontrol et",
"maintenance_integrity_check_all": "Hepsini kontrol et",
"maintenance_integrity_checksum_mismatch_description": "Disk üzerindeki sağlama toplamı, Immich'in veritabanında sakladığı sağlama toplamıyla uyuşmayan dosyalar.",
"maintenance_integrity_checksum_mismatch_job": "Sağlama toplamı uyuşmazlıklarını kontrol et",
"maintenance_integrity_missing_file": "Eksik Dosyalar",
"maintenance_integrity_missing_file_description": "Immich'in veritabanında izlediği ancak dosya sisteminde bulunmayan dosyalar.",
"maintenance_integrity_missing_file_job": "Eksik dosyaları kontrol et",
"maintenance_integrity_missing_file_refresh_job": "Eksik dosya raporlarını yenile",
"maintenance_integrity_report": "Entegrasyon Raporu",
"maintenance_integrity_untracked_file": "İzlenmeyen Dosyalar",
"maintenance_integrity_untracked_file_description": "Immich'in dizinlerinde bulunan ancak Immich'in hiçbir kaydının bulunmadığı dosyalar.",
"maintenance_integrity_untracked_file_job": "İzlenmeyen dosyaları kontrol et",
"maintenance_integrity_untracked_file_refresh_job": "İzlenmeyen dosya raporlarını yenile",
"maintenance_restore_backup": "Yedeği Geri Yükle",
"maintenance_restore_backup_description": "Immich tamamen silinecek ve seçilen yedekten geri yüklenecektir. İşleme devam etmeden önce bir yedek oluşturulacaktır.",
"maintenance_restore_backup_different_version": "Bu yedek, Immichin farklı bir sürümüyle oluşturulmuş!",
@@ -305,6 +320,7 @@
"refreshing_all_libraries": "Tüm kütüphaneler yenileniyor",
"registration": "Yönetici Kaydı",
"registration_description": "Sistemdeki ilk kullanıcı olduğunuz için hesabınız Yönetici olarak ayarlandı. Yeni oluşturulan üyeliklerin, ve yönetici görevlerinin sorumlusu olarak atandınız.",
"release_channel_release_candidate": "Yayın adayı",
"release_channel_stable": "Stabil",
"remove_failed_jobs": "Başarısız işleri kaldır",
"require_password_change_on_login": "Kullanıcının ilk girişinde şifre değiştirmesini zorunlu kıl",
@@ -400,6 +416,9 @@
"transcoding_preferred_hardware_device_description": "Sadece VAAPI ve QSV için uygulanır. Donanım kod çevrimi için DRI Node ayarlar.",
"transcoding_preset_preset": "Ön ayar (-ön)",
"transcoding_preset_preset_description": "Sıkıştırma hızı. Daha yavaş olan ayarlar belirli bitrate ayarları için daha küçük ve daha kaliteli dosya üretir. VP9 ayarı 'daha hızlı' ayarının üstündeki ayarları görmezden gelir.",
"transcoding_realtime": "Gerçek Zamanlı Kod Dönüştürme [DENEYSEL]",
"transcoding_realtime_enabled": "Gerçek zamanlı kod dönüştürmeyi etkinleştirin",
"transcoding_realtime_enabled_description": "Devre dışı bırakılırsa, sunucu yeni gerçek zamanlı kod dönüştürme oturumları başlatmayı reddedecektir.",
"transcoding_reference_frames": "Referans kareler",
"transcoding_reference_frames_description": "Belirli bir kareyi sıkıştırırken referans alınacak kare sayısı. Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. 0 bu değeri otomatik olarak ayarlar.",
"transcoding_required_description": "Yalnızca kabul edilen formatta olmayan videolar",
@@ -443,6 +462,8 @@
"user_settings_description": "Kullanıcı ayarlarını yönet",
"user_successfully_removed": "Kullanıcı {email} başarıyla kaldırıldı.",
"users_page_description": "Yönetici kullanıcılar sayfası",
"version_check_channel": "Yayın kanalı",
"version_check_channel_description": "Sürüm duyurularını almak istediğiniz yayın kanalını seçin",
"version_check_enabled_description": "Sürüm kontrolü etkin",
"version_check_implications": "Sürüm kontrol özelliği, {server} ile periyodik iletişime dayanır",
"version_check_settings": "Sürüm Kontrolü",
@@ -692,6 +713,7 @@
"backup_settings_subtitle": "Yükleme ayarlarını yönet",
"backup_upload_details_page_more_details": "Daha fazla ayrıntı için dokunun",
"backward": "Geriye doğru",
"battery_optimization_backup_reliability": "Pil optimizasyonlarını devre dışı bırakmak, arka plan yedeklemesinin güvenilirliğini artırabilir",
"biometric_auth_enabled": "Biyometrik kimlik doğrulama etkin",
"biometric_locked_out": "Biyometrik kimlik doğrulaması kilitli",
"biometric_no_options": "Biyometrik seçenek yok",
@@ -733,6 +755,7 @@
"cannot_update_the_description": "Açıklama güncellenemiyor",
"cast": "Yansıt",
"cast_description": "Kullanılabilir yansıtma hedeflerini yapılandır",
"change": "Değiştir",
"change_date": "Tarihi değiştir",
"change_description": "Açıklamayı değiştir",
"change_display_order": "Görüntüleme sırasını değiştir",
@@ -779,6 +802,7 @@
"clear": "Temizle",
"clear_all": "Hepsini temizle",
"clear_all_recent_searches": "Son aramaların hepsini temizle",
"clear_failed_count": "Temizleme başarısız oldu ({count})",
"clear_file_cache": "Dosya Önbelleğini Temizle",
"clear_message": "Mesajı temizle",
"clear_value": "Değeri temizle",
@@ -904,6 +928,7 @@
"deduplicate_all": "Tüm kopyaları kaldır",
"default_locale": "Varsayılan Dil",
"default_locale_description": "Tarih ve sayıları tarayıcınızın yerel ayarlarına göre biçimlendirin",
"default_share_quality": "Varsayılan paylaşım kalitesi",
"delete": "Sil",
"delete_action_confirmation_message": "Bu öğeyi silmek istediğinizden emin misiniz? Bu işlem, öğeyi sunucunun çöp kutusuna taşıyacak ve yerel olarak silmek isteyip istemediğinizi soracaktır",
"delete_action_prompt": "{count} silindi",
@@ -977,8 +1002,10 @@
"downloading_asset_filename": "Öğe indiriliyor {filename}",
"downloading_from_icloud": "iClouddan indiriliyor",
"downloading_media": "Medya indiriliyor",
"drag_to_reorder": "Sırayı değiştirmek için sürükleyin",
"drop_files_to_upload": "Dosyaları yüklemek için herhangi bir yere bırakın",
"duplicate": "Kopyala",
"duplicate_workflow": "İş akışını kopyala",
"duplicates": "Kopyalar",
"duplicates_description": "Her bir grubu, varsa tekrarlanan öğeleri belirterek çözümleyin.",
"duration": "Süre",
@@ -1080,6 +1107,7 @@
"failed_to_remove_product_key": "Ürün anahtarı kaldırılamadı",
"failed_to_reset_pin_code": "PIN kodu sıfırlanamadı",
"failed_to_stack_assets": "Öğeler yığınlanamadı",
"failed_to_tag_assets": "Varlıkları etiketleme başarısız oldu",
"failed_to_unstack_assets": "Öğelerin yığını kaldırılamadı",
"failed_to_update_notification_status": "Bildirim durumu güncellenemedi",
"incorrect_email_or_password": "Yanlış e-posta veya şifre",
@@ -1204,10 +1232,12 @@
"external_libraries": "Harici kütüphaneler",
"external_network": "Harici ağlar",
"external_network_sheet_info": "Belirlenmiş Wi-Fi ağına bağlı olmadığında uygulama, yukarıdan aşağıya doğru ulaşabileceği aşağıdaki URL'lerden ilki aracılığıyla sunucuya bağlanacaktır",
"f_number": "F-Numarası",
"face_unassigned": "Yüz atanmadı",
"failed": "Başarısız",
"failed_count": "Başarısız: {count}",
"failed_to_authenticate": "Kimlik doğrulaması yapılamadı",
"failed_to_delete_file": "Dosya silme işlemi başarısız oldu",
"failed_to_load_assets": "Öğeler yüklenemedi",
"failed_to_load_folder": "Klasör yüklenemedi",
"favorite": "Favori",
@@ -1338,6 +1368,7 @@
"individual_share": "Bireysel paylaşım",
"individual_shares": "Kişisel paylaşımlar",
"info": "Bilgi",
"integrity_checks": "Bütünlük Kontrolleri",
"interval": {
"day_at_onepm": "Her gün saat 13:00'te",
"hours": "{hours, plural, one {Her saat} other {Her {hours, number} saatte}}",
@@ -1385,6 +1416,7 @@
"leave": "Ayrıl",
"leave_album": "Albümden çık",
"lens_model": "Mercek modeli",
"less": "Daha az",
"let_others_respond": "Diğerlerinin yanıt vermesine izin ver",
"level": "Seviye",
"library": "Kütüphane",
@@ -1409,6 +1441,7 @@
"linked_oauth_account": "Bağlı OAuth hesabı",
"list": "Liste",
"live": "Canlı",
"load_more": "Daha Fazla Yükle",
"loading": "Yükleniyor",
"loading_search_results_failed": "Arama sonuçları yüklenemedi",
"local": "Yerel",
@@ -1509,7 +1542,6 @@
"map_location_picker_page_use_location": "Bu konumu kullan",
"map_location_service_disabled_content": "Mevcut konumunuzdan öğeleri görüntülemek için konum hizmetinin etkinleştirilmesi gerekiyor. Şimdi etkinleştirmek istiyor musunuz?",
"map_location_service_disabled_title": "Konum hizmeti devre dışı bırakıldı",
"map_marker_for_images": "{city}, {country} şehrinde çekilen fotoğraflar için harita işaretleyicisi",
"map_marker_with_image": "Resimli harita işaretleyicisi",
"map_no_location_permission_content": "Mevcut konumunuzdan öğeleri görüntülemek için konum iznine ihtiyaç var. Şimdi izin vermek istiyor musunuz?",
"map_no_location_permission_title": "Konum izni reddedildi",
@@ -1532,8 +1564,11 @@
"matching_assets": "Eşleşen Öğeler",
"media_chrome": {
"auto": "Otomatik",
"captions": "Altyazılar",
"captions_off": "Kapalı",
"closed_captions": "kapalı altyazılar",
"decode_error": "Kod çözümleme hatası",
"disable_captions": "Altyazıları devre dışı bırak",
"enable_captions": "Altyazılar açık",
"enter_fullscreen_mode": "Tam ekran kipini aç",
"exit_fullscreen_mode": "Tam ekran kipini kapat",
@@ -1553,6 +1588,8 @@
"seconds": "saniyeler",
"time_value_of_total_time": "{currentTime} / {totalTime}",
"time_value_remaining": "{time} kaldı",
"unmute": "Sesini açmak",
"unsupported_error_description": "Desteklenmeyen bir hata oluştu. Sunucu veya ağ hatası ya da tarayıcınız bu formatı desteklemiyor.",
"video_not_loaded_unknown_time": "video yüklenmedi, süre bilinmiyor.",
"video_player": "Video oynatıcı",
"volume": "Ses"
@@ -1573,6 +1610,8 @@
"merge_people_prompt": "Bu kişileri birleştirmek istiyor musunuz? Bu işlem geri alınamaz.",
"merge_people_successfully": "Kişiler başarılı bir şekilde birleştirildi",
"merged_people_count": "{count, plural, one {# kişi} other {# kişi}} birleştirildi",
"minFaces": "Minimum yüzler",
"minFaces_description": "Bir kişinin görüntülenebilmesi için minimum tanınan yüz sayısı",
"minimize": "Küçült",
"minute": "Dakika",
"minutes": "Dakika",
@@ -1659,6 +1698,7 @@
"no_results": "Sonuç bulunamadı",
"no_results_description": "Eş anlamlı ya da daha genel anlamlı bir kelime deneyin",
"no_shared_albums_message": "Fotoğrafları ve videoları ağınızdaki kişilerle paylaşmak için bir albüm oluşturun",
"no_steps": "Henüz hiçbir adım eklenmedi",
"no_uploads_in_progress": "Yükleme işlemi yok",
"none": "Yok",
"not_allowed": "İzin verilmiyor",
@@ -1667,6 +1707,7 @@
"not_selected": "Seçilmedi",
"notes": "Notlar",
"nothing_here_yet": "Burada henüz bir şey yok",
"notification_backup_reliability": "Arka plan yedeklemelerinin güvenirliğini iyileştirmek için bildirimlere izin verin",
"notification_permission_dialog_content": "Bildirimleri etkinleştirmek için cihaz ayarlarına gidin ve izin verin.",
"notification_permission_list_tile_content": "Bildirimleri etkinleştirmek için izin verin.",
"notification_permission_list_tile_enable_button": "Bildirimleri Etkinleştir",
@@ -1796,6 +1837,7 @@
"play_transcoded_video": "Kodlanmış videoyu oynat",
"please_auth_to_access": "Erişim için lütfen kimliğinizi doğrulayın",
"plugin_method_filter_type": "Süzgeç",
"plugin_method_filter_type_description": "Bu yöntem olayları filtreleyebilir ve koşullu olarak sonraki adımların çalışmasını engelleyebilir",
"port": "Port",
"preferences_settings_subtitle": "Uygulama tercihlerini düzenle",
"preferences_settings_title": "Tercihler",
@@ -1978,6 +2020,8 @@
"search_by_description_example": "Sapa'da yürüyüş günü",
"search_by_filename": "Dosya adına veya uzantısına göre ara",
"search_by_filename_example": "Örn. IMG_1234.JPG veya PNG",
"search_by_full_path": "Tam dosya yolu veya klasöre göre arama yapın",
"search_by_full_path_example": "/John/Projeler/3D_Baskı/2026-07-01 - Projeler, 3D, Baskı, 2026 vb. kelimelerle arama yapabilirsiniz.",
"search_by_ocr": "OCR'ye göre ara",
"search_by_ocr_example": "Sütlü Kahve",
"search_camera_lens_model": "Lens modelini ara...",
@@ -2054,6 +2098,7 @@
"select_person": "Kişileri seç",
"select_person_to_tag": "Etiketlemek için bir kişi seçin",
"select_photos": "Fotoğrafları seç",
"select_quality": "Kaliteyi seçin",
"select_trash_all": "Hepsini çöpe at",
"select_user_for_sharing_page_err_album": "Albüm oluşturulamadı",
"selected": "Seçildi",
@@ -2117,6 +2162,8 @@
"share_assets_selected": "{count} seçili",
"share_dialog_preparing": "Hazırlanıyor...",
"share_link": "Bağlantıyı Paylaş",
"share_original": "Orijinal (büyük) olanı kullanın",
"share_preview": "Küçük resmi kullan",
"shared": "Paylaşılan",
"shared_album_activities_input_disable": "Yoruma kapalı",
"shared_album_activity_remove_content": "Bu etkinliği silmek istiyor musunuz?",
@@ -2210,12 +2257,14 @@
"skip_to_folders": "Klasörlere atla",
"skip_to_tags": "Etiketlere atla",
"slideshow": "Slayt gösterisi",
"slideshow_metadata_overlay_mode": "Yer paylaşımı içeriği",
"slideshow_metadata_overlay_mode_description_only": "Sadece açıklama",
"slideshow_metadata_overlay_mode_full": "Dolu",
"slideshow_repeat": "Slayt gösterisini tekrarla",
"slideshow_repeat_description": "Slayt gösterisi bittiğinde başa dön",
"slideshow_settings": "Slayt gösterisi ayarları",
"smart_album": "Akıllı albüm",
"some_assets_already_have_a_location_warning": "Seçilen varlıkların bazılarının zaten bir konumu mevcut",
"sort_albums_by": "Albümleri sırala...",
"sort_created": "Oluşturulma tarihi",
"sort_items": "Öğe sayısı",
@@ -2239,6 +2288,7 @@
"state": "Eyalet/İl",
"status": "Durum",
"step_delete": "Adımı sil",
"step_delete_confirm": "Bu adımı silmek istediğinizden emin misiniz?",
"step_details": "Adım ayrıntıları",
"steps": "Adımlar",
"stop_casting": "Yansıtmayı durdur",
@@ -2334,11 +2384,13 @@
"trash_page_title": "Çöp Kutusu ({count})",
"trashed_items_will_be_permanently_deleted_after": "Silinen öğeler {days, plural, one {# gün} other {# gün}} sonra kalıcı olarak silinecek.",
"trigger": "Tetikleyici",
"trigger_asset_uploaded": "Öğe Karşıya Yüklendi",
"trigger_asset_metadata_extraction": "Varlık Meta Veri Çıkarma",
"trigger_asset_metadata_extraction_description": "Bir varlığın EXIF meta verileri çıkarıldığında tetiklenir",
"trigger_asset_uploaded": "Öğe Karşıya Yüklenince",
"trigger_asset_uploaded_description": "Yeni bir öğe karşıya yüklendiğinde tetiklenir",
"trigger_description": "İş akışını başlatan bir olay",
"trigger_person_recognized": "Tanınan Kişi",
"trigger_person_recognized_description": "Bir kişi algılandığında tetiklenir",
"trigger_person_recognized_description": "Bir kişi tanındığında tetiklenir",
"trigger_type": "Tetikleyici türü",
"troubleshoot": "Sorun giderme",
"type": "Tür",
@@ -2380,6 +2432,7 @@
"updated_password": "Güncellenen şifre",
"upload": "Yükle",
"upload_concurrency": "Yükleme eşzamanlılığı",
"upload_day_count": "{tarih}: {sayı, çoğul, bir {# yükleme} diğer {# yüklemeler}}",
"upload_details": "Yükleme Ayrıntıları",
"upload_dialog_info": "Seçili öğeleri sunucuya yedeklemek istiyor musunuz?",
"upload_dialog_title": "Öğe Yükle",
@@ -2395,6 +2448,7 @@
"upload_to_immich": "Immich'e Yükle ({count})",
"uploading": "Yükleniyor",
"uploading_media": "Medya yükleme",
"uploads": "Yüklemeler",
"url": "URL",
"usage": "Kullanım",
"use_biometric": "Biyometri kullan",
@@ -2402,6 +2456,7 @@
"use_browser_locale_description": "Tarih, saat ve sayılar tarayıcınızın yerel ayarlarına göre biçimlendirilsin",
"use_current_connection": "Mevcut bağlantıyı kullan",
"use_custom_date_range": "Bunun yerine özel tarih aralığını kullan",
"use_template": "Şablonu kullan",
"user": "Kullanıcı",
"user_has_been_deleted": "Bu kullanıcı silindi.",
"user_id": "Kullanıcı ID",
@@ -2431,6 +2486,7 @@
"video": "Video",
"video_hover_setting": "Üzerinde durulduğunda video ön izlemesi oynat",
"video_hover_setting_description": "Öğe üzerinde fareyle durulduğunda video küçük resmini oynatır. Bu özellik devre dışıyken, oynatma simgesine fareyle gidilerek oynatma başlatılabilir.",
"video_quality": "Video kalitesi",
"videos": "Videolar",
"videos_count": "{count, plural, one {# video} other {# video}}",
"videos_only": "Sadece videolar",
@@ -2463,6 +2519,7 @@
"week": "Hafta",
"welcome": "Hoş geldiniz",
"welcome_to_immich": "Immich'e hoş geldiniz",
"when": "Ne zaman",
"width": "Genişlik",
"wifi_name": "Wi-Fi Adı",
"workflow": "İş Akışı",
@@ -2475,6 +2532,7 @@
"workflow_name": "İş akışı adı",
"workflow_navigation_prompt": "Değişikliklerinizi kaydetmeden ayrılmak istediğinizden emin misiniz?",
"workflow_summary": "İş akışı özeti",
"workflow_templates": "İş akışı şablonları",
"workflow_update_success": "İş akışı başarıyla güncellendi",
"workflow_updated": "İş akışı güncellendi",
"workflows": "İş akışları",
-1
View File
@@ -1532,7 +1532,6 @@
"map_location_picker_page_use_location": "Використати це місце",
"map_location_service_disabled_content": "Служба визначення місця має бути увімкнена, щоб відображати елементи з вашого поточного місця. Увімкнути її зараз?",
"map_location_service_disabled_title": "Служба визначення місця вимкнена",
"map_marker_for_images": "Маркер на мапі для зображень, знятих у {city}, {country}",
"map_marker_with_image": "Маркер на мапі із зображенням",
"map_no_location_permission_content": "Потрібен дозвіл, щоб показувати елементи із поточного місця. Надати його зараз?",
"map_no_location_permission_title": "Доступ до місця не надано",
+5
View File
@@ -27,6 +27,7 @@
"add_partner": "ساتھی شامل کریں",
"add_path": "راستہ شامل کریں",
"add_photos": "تصاویر شامل کریں",
"add_step": "مرحلہ بنائیں",
"add_tag": "ٹیگ شامل کریں",
"add_to": "اس میں شامل کریں…",
"add_to_album": "البم میں شامل کریں",
@@ -70,7 +71,11 @@
"confirm_reprocess_all_faces": "کیا آپ واقعی تمام چہروں کو دوبارہ پروسیس کرنا چاہتے ہیں؟ اس سے نام والے افراد بھی صاف ہو جائیں گے۔",
"confirm_user_password_reset": "کیا آپ {user} کا پاس ورڈ ری سیٹ کرنا چاہتے ہیں؟",
"confirm_user_pin_code_reset": "کیا آپ {user} کا پن کوڈ ری سیٹ کرنا چاہتے ہیں؟",
"copy_config_to_clipboard_description": "اپنی موجودہ سسٹم کے نظام کی ترتیب JSON کی شکل میں کلپ بورڈ میں کاپی کریں",
"create_job": "کام بنائیں",
"disable_login": "لاگ ان بند کریں",
"download_csv": "CSV ڈاون لوڈ کریں",
"duplicate_detection_job_description": "اپنی اجراۂ پر مشین لرننگ چلا کر ایک جیسی تصاویر کا پتہ لگا ئے۔ \"Smart Search\" پر انحصار کرتا ہے",
"face_detection": "چہرے کی پہچان",
"failed_job_command": "کام: {job} کے لیے کمانڈ: {command} ناکام ہو گئی",
"image_preview_title": "پیش نظارہ",
+491 -356
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -304,6 +304,12 @@
"oauth_storage_label_claim": "儲存標籤宣告",
"oauth_storage_label_claim_description": "自動將使用者嘅儲存標籤設定為呢個宣告值。",
"oauth_storage_quota_claim": "儲存限額宣告",
"oauth_storage_quota_claim_description": "自動將使用者嘅儲存限額設定為呢個宣告之值。",
"oauth_storage_quota_default": "預設儲存限額(GiB",
"oauth_storage_quota_default_description": "未提供宣告時所使用嘅限額(GiB)。",
"oauth_timeout": "請求超時",
"oauth_timeout_description": "請求超時(毫秒)",
"ocr_job_description": "用機械學習辨識影像中嘅文字",
"queue_details": "隊列資訊",
"queues": "任務隊列",
"queues_page_description": "管理員任務隊列頁面",
-1
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "使用此位置",
"map_location_service_disabled_content": "需要启用定位服务才能显示您当前位置的媒体文件。是否现在启用它?",
"map_location_service_disabled_title": "定位服务已禁用",
"map_marker_for_images": "标记{city}、{country}拍摄照片的地图图标",
"map_marker_with_image": "带预览图的地图标记",
"map_no_location_permission_content": "需要位置权限才能显示您当前位置的照片/视频。现在要允许吗?",
"map_no_location_permission_title": "位置权限被拒绝",
-1
View File
@@ -1548,7 +1548,6 @@
"map_location_picker_page_use_location": "使用此位置",
"map_location_service_disabled_content": "需要啟用定位服務才能顯示您目前位置相關的項目。要現在啟用嗎?",
"map_location_service_disabled_title": "定位服務已停用",
"map_marker_for_images": "在 {city}、{country} 拍攝影像的地圖標記",
"map_marker_with_image": "帶有影像的地圖標記",
"map_no_location_permission_content": "需要位置權限才能顯示與您目前位置相關的項目。要現在就授予位置權限嗎?",
"map_no_location_permission_title": "沒有位置權限",
@@ -207,18 +207,6 @@ enum class PlatformAssetPlaybackStyle(val raw: Int) {
}
}
enum class EditState(val raw: Int) {
NOT_EDITED(0),
EDITED(1),
UNKNOWN(2);
companion object {
fun ofRaw(raw: Int): EditState? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class PlatformAsset (
val id: String,
@@ -234,9 +222,7 @@ data class PlatformAsset (
val adjustmentTime: Long? = null,
val latitude: Double? = null,
val longitude: Double? = null,
val playbackStyle: PlatformAssetPlaybackStyle,
val burstId: String? = null,
val isBurstRepresentative: Boolean
val playbackStyle: PlatformAssetPlaybackStyle
)
{
companion object {
@@ -255,9 +241,7 @@ data class PlatformAsset (
val latitude = pigeonVar_list[11] as Double?
val longitude = pigeonVar_list[12] as Double?
val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle
val burstId = pigeonVar_list[14] as String?
val isBurstRepresentative = pigeonVar_list[15] as Boolean
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationMs, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle, burstId, isBurstRepresentative)
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationMs, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
}
}
fun toList(): List<Any?> {
@@ -276,8 +260,6 @@ data class PlatformAsset (
latitude,
longitude,
playbackStyle,
burstId,
isBurstRepresentative,
)
}
override fun equals(other: Any?): Boolean {
@@ -288,7 +270,7 @@ data class PlatformAsset (
return true
}
val other = other as PlatformAsset
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationMs, other.durationMs) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle) && MessagesPigeonUtils.deepEquals(this.burstId, other.burstId) && MessagesPigeonUtils.deepEquals(this.isBurstRepresentative, other.isBurstRepresentative)
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationMs, other.durationMs) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle)
}
override fun hashCode(): Int {
@@ -307,8 +289,6 @@ data class PlatformAsset (
result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude)
result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude)
result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle)
result = 31 * result + MessagesPigeonUtils.deepHash(this.burstId)
result = 31 * result + MessagesPigeonUtils.deepHash(this.isBurstRepresentative)
return result
}
}
@@ -492,82 +472,6 @@ data class CloudIdResult (
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class BaseResource (
val path: String,
val sha1: String
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): BaseResource {
val path = pigeonVar_list[0] as String
val sha1 = pigeonVar_list[1] as String
return BaseResource(path, sha1)
}
}
fun toList(): List<Any?> {
return listOf(
path,
sha1,
)
}
override fun equals(other: Any?): Boolean {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
val other = other as BaseResource
return MessagesPigeonUtils.deepEquals(this.path, other.path) && MessagesPigeonUtils.deepEquals(this.sha1, other.sha1)
}
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.path)
result = 31 * result + MessagesPigeonUtils.deepHash(this.sha1)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class BaseLivePhoto (
val still: BaseResource,
val video: BaseResource? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): BaseLivePhoto {
val still = pigeonVar_list[0] as BaseResource
val video = pigeonVar_list[1] as BaseResource?
return BaseLivePhoto(still, video)
}
}
fun toList(): List<Any?> {
return listOf(
still,
video,
)
}
override fun equals(other: Any?): Boolean {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
val other = other as BaseLivePhoto
return MessagesPigeonUtils.deepEquals(this.still, other.still) && MessagesPigeonUtils.deepEquals(this.video, other.video)
}
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.still)
result = 31 * result + MessagesPigeonUtils.deepHash(this.video)
return result
}
}
private open class MessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -577,45 +481,30 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
}
}
130.toByte() -> {
return (readValue(buffer) as Long?)?.let {
EditState.ofRaw(it.toInt())
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformAsset.fromList(it)
}
}
132.toByte() -> {
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformAlbum.fromList(it)
}
}
133.toByte() -> {
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SyncDelta.fromList(it)
}
}
134.toByte() -> {
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HashResult.fromList(it)
}
}
135.toByte() -> {
134.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CloudIdResult.fromList(it)
}
}
136.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
BaseResource.fromList(it)
}
}
137.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
BaseLivePhoto.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
@@ -625,36 +514,24 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
stream.write(129)
writeValue(stream, value.raw.toLong())
}
is EditState -> {
stream.write(130)
writeValue(stream, value.raw.toLong())
}
is PlatformAsset -> {
stream.write(131)
stream.write(130)
writeValue(stream, value.toList())
}
is PlatformAlbum -> {
stream.write(132)
stream.write(131)
writeValue(stream, value.toList())
}
is SyncDelta -> {
stream.write(133)
stream.write(132)
writeValue(stream, value.toList())
}
is HashResult -> {
stream.write(134)
stream.write(133)
writeValue(stream, value.toList())
}
is CloudIdResult -> {
stream.write(135)
writeValue(stream, value.toList())
}
is BaseResource -> {
stream.write(136)
writeValue(stream, value.toList())
}
is BaseLivePhoto -> {
stream.write(137)
stream.write(134)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
@@ -679,18 +556,6 @@ interface NativeSyncApi {
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
fun getBaseResource(assetId: String, allowNetworkAccess: Boolean, callback: (Result<BaseResource?>) -> Unit)
/**
* Streams the bytes immich treats as the asset's canonical content the same
* resource [hashAssets] hashes (`PHAsset.getResource()`, the `.isCurrent`
* rendition). Used to upload iOS burst members: they're invisible to
* photo_manager, so this is the only way to read their file, and streaming
* the same resource the hash measured keeps the server checksum aligned with
* the local one (else the asset shows cloud-only). iOS-only; android returns null.
*/
fun getCurrentResource(assetId: String, allowNetworkAccess: Boolean, callback: (Result<BaseResource?>) -> Unit)
fun getEditState(assetId: String, allowNetworkAccess: Boolean, callback: (Result<EditState>) -> Unit)
fun getBaseLivePhoto(assetId: String, allowNetworkAccess: Boolean, callback: (Result<BaseLivePhoto?>) -> Unit)
companion object {
/** The codec used by NativeSyncApi. */
@@ -953,90 +818,6 @@ interface NativeSyncApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getBaseResource$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val assetIdArg = args[0] as String
val allowNetworkAccessArg = args[1] as Boolean
api.getBaseResource(assetIdArg, allowNetworkAccessArg) { result: Result<BaseResource?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCurrentResource$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val assetIdArg = args[0] as String
val allowNetworkAccessArg = args[1] as Boolean
api.getCurrentResource(assetIdArg, allowNetworkAccessArg) { result: Result<BaseResource?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getEditState$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val assetIdArg = args[0] as String
val allowNetworkAccessArg = args[1] as Boolean
api.getEditState(assetIdArg, allowNetworkAccessArg) { result: Result<EditState> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getBaseLivePhoto$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val assetIdArg = args[0] as String
val allowNetworkAccessArg = args[1] as Boolean
api.getBaseLivePhoto(assetIdArg, allowNetworkAccessArg) { result: Result<BaseLivePhoto?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -204,8 +204,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
0L,
isFavorite,
playbackStyle = playbackStyle,
// Android has no burstIdentifier equivalent in MediaStore — bursts are iOS-only.
isBurstRepresentative = false,
)
yield(AssetResult.ValidAsset(asset, bucketId))
}
@@ -511,25 +509,4 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
return emptyList()
}
// Android has no Photos-style edit original to stack; iOS-only.
fun getBaseResource(assetId: String, allowNetworkAccess: Boolean, callback: (Result<BaseResource?>) -> Unit) {
completeWhenActive(callback, Result.success(null))
}
// iOS-only; burst members are an iOS concept. Android resolves every asset via
// MediaStore already, so there's no hidden-member byte fetch to provide.
fun getCurrentResource(assetId: String, allowNetworkAccess: Boolean, callback: (Result<BaseResource?>) -> Unit) {
completeWhenActive(callback, Result.success(null))
}
// iOS-only; Android assets never carry a Photos-style edit.
fun getEditState(assetId: String, allowNetworkAccess: Boolean, callback: (Result<EditState>) -> Unit) {
completeWhenActive(callback, Result.success(EditState.NOT_EDITED))
}
// iOS-only; Android assets never carry a Photos-style live edit.
fun getBaseLivePhoto(assetId: String, allowNetworkAccess: Boolean, callback: (Result<BaseLivePhoto?>) -> Unit) {
completeWhenActive(callback, Result.success(null))
}
}
File diff suppressed because it is too large Load Diff
+11 -214
View File
@@ -183,12 +183,6 @@ enum PlatformAssetPlaybackStyle: Int {
case videoLooping = 5
}
enum EditState: Int {
case notEdited = 0
case edited = 1
case unknown = 2
}
/// Generated class from Pigeon that represents data sent in messages.
struct PlatformAsset: Hashable {
var id: String
@@ -205,8 +199,6 @@ struct PlatformAsset: Hashable {
var latitude: Double? = nil
var longitude: Double? = nil
var playbackStyle: PlatformAssetPlaybackStyle
var burstId: String? = nil
var isBurstRepresentative: Bool
// swift-format-ignore: AlwaysUseLowerCamelCase
@@ -225,8 +217,6 @@ struct PlatformAsset: Hashable {
let latitude: Double? = nilOrValue(pigeonVar_list[11])
let longitude: Double? = nilOrValue(pigeonVar_list[12])
let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle
let burstId: String? = nilOrValue(pigeonVar_list[14])
let isBurstRepresentative = pigeonVar_list[15] as! Bool
return PlatformAsset(
id: id,
@@ -242,9 +232,7 @@ struct PlatformAsset: Hashable {
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
playbackStyle: playbackStyle,
burstId: burstId,
isBurstRepresentative: isBurstRepresentative
playbackStyle: playbackStyle
)
}
func toList() -> [Any?] {
@@ -263,15 +251,13 @@ struct PlatformAsset: Hashable {
latitude,
longitude,
playbackStyle,
burstId,
isBurstRepresentative,
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationMs, rhs.durationMs) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle) && deepEqualsMessages(lhs.burstId, rhs.burstId) && deepEqualsMessages(lhs.isBurstRepresentative, rhs.isBurstRepresentative)
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationMs, rhs.durationMs) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle)
}
func hash(into hasher: inout Hasher) {
@@ -290,8 +276,6 @@ struct PlatformAsset: Hashable {
deepHashMessages(value: latitude, hasher: &hasher)
deepHashMessages(value: longitude, hasher: &hasher)
deepHashMessages(value: playbackStyle, hasher: &hasher)
deepHashMessages(value: burstId, hasher: &hasher)
deepHashMessages(value: isBurstRepresentative, hasher: &hasher)
}
}
@@ -474,78 +458,6 @@ struct CloudIdResult: Hashable {
}
}
/// Generated class from Pigeon that represents data sent in messages.
struct BaseResource: Hashable {
var path: String
var sha1: String
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> BaseResource? {
let path = pigeonVar_list[0] as! String
let sha1 = pigeonVar_list[1] as! String
return BaseResource(
path: path,
sha1: sha1
)
}
func toList() -> [Any?] {
return [
path,
sha1,
]
}
static func == (lhs: BaseResource, rhs: BaseResource) -> Bool {
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.path, rhs.path) && deepEqualsMessages(lhs.sha1, rhs.sha1)
}
func hash(into hasher: inout Hasher) {
hasher.combine("BaseResource")
deepHashMessages(value: path, hasher: &hasher)
deepHashMessages(value: sha1, hasher: &hasher)
}
}
/// Generated class from Pigeon that represents data sent in messages.
struct BaseLivePhoto: Hashable {
var still: BaseResource
var video: BaseResource? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> BaseLivePhoto? {
let still = pigeonVar_list[0] as! BaseResource
let video: BaseResource? = nilOrValue(pigeonVar_list[1])
return BaseLivePhoto(
still: still,
video: video
)
}
func toList() -> [Any?] {
return [
still,
video,
]
}
static func == (lhs: BaseLivePhoto, rhs: BaseLivePhoto) -> Bool {
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.still, rhs.still) && deepEqualsMessages(lhs.video, rhs.video)
}
func hash(into hasher: inout Hasher) {
hasher.combine("BaseLivePhoto")
deepHashMessages(value: still, hasher: &hasher)
deepHashMessages(value: video, hasher: &hasher)
}
}
private class MessagesPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
@@ -556,25 +468,15 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
}
return nil
case 130:
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
return EditState(rawValue: enumResultAsInt)
}
return nil
case 131:
return PlatformAsset.fromList(self.readValue() as! [Any?])
case 132:
case 131:
return PlatformAlbum.fromList(self.readValue() as! [Any?])
case 133:
case 132:
return SyncDelta.fromList(self.readValue() as! [Any?])
case 134:
case 133:
return HashResult.fromList(self.readValue() as! [Any?])
case 135:
case 134:
return CloudIdResult.fromList(self.readValue() as! [Any?])
case 136:
return BaseResource.fromList(self.readValue() as! [Any?])
case 137:
return BaseLivePhoto.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
@@ -586,29 +488,20 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter {
if let value = value as? PlatformAssetPlaybackStyle {
super.writeByte(129)
super.writeValue(value.rawValue)
} else if let value = value as? EditState {
super.writeByte(130)
super.writeValue(value.rawValue)
} else if let value = value as? PlatformAsset {
super.writeByte(131)
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? PlatformAlbum {
super.writeByte(132)
super.writeByte(131)
super.writeValue(value.toList())
} else if let value = value as? SyncDelta {
super.writeByte(133)
super.writeByte(132)
super.writeValue(value.toList())
} else if let value = value as? HashResult {
super.writeByte(134)
super.writeByte(133)
super.writeValue(value.toList())
} else if let value = value as? CloudIdResult {
super.writeByte(135)
super.writeValue(value.toList())
} else if let value = value as? BaseResource {
super.writeByte(136)
super.writeValue(value.toList())
} else if let value = value as? BaseLivePhoto {
super.writeByte(137)
super.writeByte(134)
super.writeValue(value.toList())
} else {
super.writeValue(value)
@@ -647,16 +540,6 @@ protocol NativeSyncApi {
func getTrashedAssets() throws -> [String: [PlatformAsset]]
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
func getBaseResource(assetId: String, allowNetworkAccess: Bool, completion: @escaping (Result<BaseResource?, Error>) -> Void)
/// Streams the bytes immich treats as the asset's canonical content the same
/// resource [hashAssets] hashes (`PHAsset.getResource()`, the `.isCurrent`
/// rendition). Used to upload iOS burst members: they're invisible to
/// photo_manager, so this is the only way to read their file, and streaming
/// the same resource the hash measured keeps the server checksum aligned with
/// the local one (else the asset shows cloud-only). iOS-only; android returns null.
func getCurrentResource(assetId: String, allowNetworkAccess: Bool, completion: @escaping (Result<BaseResource?, Error>) -> Void)
func getEditState(assetId: String, allowNetworkAccess: Bool, completion: @escaping (Result<EditState, Error>) -> Void)
func getBaseLivePhoto(assetId: String, allowNetworkAccess: Bool, completion: @escaping (Result<BaseLivePhoto?, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -890,91 +773,5 @@ class NativeSyncApiSetup {
} else {
getCloudIdForAssetIdsChannel.setMessageHandler(nil)
}
let getBaseResourceChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getBaseResource\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getBaseResource\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getBaseResourceChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let assetIdArg = args[0] as! String
let allowNetworkAccessArg = args[1] as! Bool
api.getBaseResource(assetId: assetIdArg, allowNetworkAccess: allowNetworkAccessArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
getBaseResourceChannel.setMessageHandler(nil)
}
/// Streams the bytes immich treats as the asset's canonical content the same
/// resource [hashAssets] hashes (`PHAsset.getResource()`, the `.isCurrent`
/// rendition). Used to upload iOS burst members: they're invisible to
/// photo_manager, so this is the only way to read their file, and streaming
/// the same resource the hash measured keeps the server checksum aligned with
/// the local one (else the asset shows cloud-only). iOS-only; android returns null.
let getCurrentResourceChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCurrentResource\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCurrentResource\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getCurrentResourceChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let assetIdArg = args[0] as! String
let allowNetworkAccessArg = args[1] as! Bool
api.getCurrentResource(assetId: assetIdArg, allowNetworkAccess: allowNetworkAccessArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
getCurrentResourceChannel.setMessageHandler(nil)
}
let getEditStateChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getEditState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getEditState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getEditStateChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let assetIdArg = args[0] as! String
let allowNetworkAccessArg = args[1] as! Bool
api.getEditState(assetId: assetIdArg, allowNetworkAccess: allowNetworkAccessArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
getEditStateChannel.setMessageHandler(nil)
}
let getBaseLivePhotoChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getBaseLivePhoto\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getBaseLivePhoto\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getBaseLivePhotoChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let assetIdArg = args[0] as! String
let allowNetworkAccessArg = args[1] as! Bool
api.getBaseLivePhoto(assetId: assetIdArg, allowNetworkAccess: allowNetworkAccessArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
getBaseLivePhotoChannel.setMessageHandler(nil)
}
}
}
+6 -362
View File
@@ -1,6 +1,5 @@
import Photos
import CryptoKit
import UniformTypeIdentifiers
struct AssetWrapper: Hashable, Equatable {
let asset: PlatformAsset
@@ -116,7 +115,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
options.includeHiddenAssets = false
let assets = getAssetsFromAlbum(in: album, options: options)
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
@@ -176,14 +175,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
deletedAssets.formUnion(details.deletedLocalIdentifiers)
if (updated.isEmpty) { continue }
let options = PHFetchOptions()
options.includeHiddenAssets = false
options.includeAllBurstAssets = true
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
for i in 0..<result.count {
let asset = result.object(at: i)
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
let predicate = PlatformAsset(
id: asset.localIdentifier,
@@ -192,28 +190,14 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
durationMs: 0,
orientation: 0,
isFavorite: false,
playbackStyle: .unknown,
isBurstRepresentative: false
playbackStyle: .unknown
)
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue
}
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
updatedAssets.insert(domainAsset)
// iOS reports only the representative in change details, so a delta sync
// would otherwise miss the other frames. Pull the full burst group
// explicitly and add each member (deduped by the wrapper's id).
if let burstId = asset.burstIdentifier {
let burstOptions = PHFetchOptions()
burstOptions.includeHiddenAssets = false
burstOptions.includeAllBurstAssets = true
let members = PHAsset.fetchAssets(withBurstIdentifier: burstId, options: burstOptions)
members.enumerateObjects { (member, _, _) in
updatedAssets.insert(AssetWrapper(with: member.toPlatformAsset()))
}
}
}
}
@@ -325,12 +309,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
var missingAssetIds = Set(assetIds)
var assets = [PHAsset]()
assets.reserveCapacity(assetIds.count)
// includeAllBurstAssets: a non-representative burst member is invisible to a
// default fetch-by-id, so without this it'd be reported "not found" and never
// hashed leaving it out of the backup candidate set permanently.
let hashFetchOptions = PHFetchOptions()
hashFetchOptions.includeAllBurstAssets = true
PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: hashFetchOptions).enumerateObjects { (asset, _, stop) in
PHAsset.fetchAssets(withLocalIdentifiers: assetIds, options: nil).enumerateObjects { (asset, _, stop) in
if Task.isCancelled {
stop.pointee = true
return
@@ -466,11 +445,6 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
}
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
// Surface every burst member, not just the auto-picked representative. Set in
// the shared fetch helper so album asset counts and the asset lists stay
// consistent LocalSyncService compares assetCount against the synced set, so
// a count that excludes burst members while the list includes them wedges sync.
options.includeAllBurstAssets = true
// Ensure to actually getting all assets for the Recents album
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
return PHAsset.fetchAssets(with: options)
@@ -502,334 +476,4 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
}
return mappings;
}
// Streams the asset's current rendition the same resource hashAssets hashes
// (getResource(): the isCurrent rendition, the lone .photo otherwise). Used for
// iOS burst members, which photo_manager can't resolve by id; streaming the same
// bytes the hash measured keeps the server checksum aligned with the local one
// (else the asset shows cloud-only). includeAllBurstAssets so a non-rep resolves.
func getCurrentResource(
assetId: String,
allowNetworkAccess: Bool,
completion: @escaping (Result<BaseResource?, Error>) -> Void
) {
Task { [weak self] in
guard let self = self else { return }
let options = PHFetchOptions()
options.includeAllBurstAssets = true
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: options).firstObject,
let resource = asset.getResource()
else {
return self.completeWhenActive(for: completion, with: .success(nil))
}
do {
let result = try await self.streamBaseResource(
resource: resource,
localId: assetId,
allowNetworkAccess: allowNetworkAccess
)
self.completeWhenActive(for: completion, with: .success(result))
} catch {
self.completeWhenActive(for: completion, with: .failure(error))
}
}
}
func getBaseResource(
assetId: String,
allowNetworkAccess: Bool,
completion: @escaping (Result<BaseResource?, Error>) -> Void
) {
Task { [weak self] in
guard let self = self else { return }
do {
guard let originals = try await Self.originalsForEditedAsset(assetId, allowNetworkAccess: allowNetworkAccess)
else {
return self.completeWhenActive(for: completion, with: .success(nil))
}
let result = try await self.streamBaseResource(
resource: originals.still,
localId: assetId,
allowNetworkAccess: allowNetworkAccess
)
self.completeWhenActive(for: completion, with: .success(result))
} catch {
self.completeWhenActive(for: completion, with: .failure(error))
}
}
}
// Reads both readable originals of an edited live photo (still + paired video) so the
// backup can upload the unedited pair and stack the edit onto it. Same edited-only gate
// as getBaseResource. video is nil when the asset has no paired video left to recover
// (e.g. the edit turned Live off); the still temp is removed if the video read fails.
func getBaseLivePhoto(
assetId: String,
allowNetworkAccess: Bool,
completion: @escaping (Result<BaseLivePhoto?, Error>) -> Void
) {
Task { [weak self] in
guard let self = self else { return }
do {
guard let originals = try await Self.originalsForEditedAsset(assetId, allowNetworkAccess: allowNetworkAccess)
else {
return self.completeWhenActive(for: completion, with: .success(nil))
}
let still = try await self.streamBaseResource(
resource: originals.still,
localId: assetId,
allowNetworkAccess: allowNetworkAccess
)
var video: BaseResource? = nil
if let videoRes = originals.video {
do {
video = try await self.streamBaseResource(
resource: videoRes,
localId: assetId,
allowNetworkAccess: allowNetworkAccess
)
} catch {
try? FileManager.default.removeItem(atPath: still.path)
throw error
}
}
self.completeWhenActive(for: completion, with: .success(BaseLivePhoto(still: still, video: video)))
} catch {
self.completeWhenActive(for: completion, with: .failure(error))
}
}
}
// Returns whether the asset carries a live Photos edit without reading the photo
// itself, only the small adjustment metadata. The revert probe relies on this to
// tell "not edited" apart from "couldn't read" (offloaded to iCloud), so it never
// mistakes an unreadable edit for a revert.
func getEditState(
assetId: String,
allowNetworkAccess: Bool,
completion: @escaping (Result<EditState, Error>) -> Void
) {
Task { [weak self] in
guard let self = self else { return }
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else {
// Not in the library, so don't answer "not edited" (the caller acts on that).
return self.completeWhenActive(for: completion, with: .success(.unknown))
}
let state = await Self.classifyEdit(
resources: PHAssetResource.assetResources(for: asset),
allowNetworkAccess: allowNetworkAccess
)
self.completeWhenActive(for: completion, with: .success(state))
}
}
// adjustmentRenderTypes for a photo with no real edit: a plain capture, a
// Photographic Style, or a reverted edit. A real edit changes this value.
private static let kNoEditRenderTypes = 27648
// Idle deadline for the base-resource reads: cancel only after this long with no
// data received, so a stalled iCloud fetch can't hang the backup forever but a
// big original on a slow link keeps downloading as long as chunks flow.
private static let kBaseReadTimeoutSeconds: Double = 120
private final class ResourceRequestRef {
var id: PHAssetResourceDataRequestID?
// Written from the resource callback queue, read from the deadline timer;
// unsynchronized on purpose the read below clamps, so the worst case is
// the timer re-arming one extra round.
var lastActivity = DispatchTime.now()
}
// Re-arming watchdog: fires after `delay`, cancels if nothing arrived for a full
// timeout window, otherwise re-arms for the remainder of the window.
private static func armIdleDeadline(_ ref: ResourceRequestRef, after delay: Double = kBaseReadTimeoutSeconds) {
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
guard let id = ref.id else { return }
let nowNs = DispatchTime.now().uptimeNanoseconds
let lastNs = ref.lastActivity.uptimeNanoseconds
// lastActivity can race ahead of the captured now; treat that as activity.
let idle = nowNs > lastNs ? Double(nowNs - lastNs) / 1_000_000_000 : 0
if idle >= kBaseReadTimeoutSeconds {
PHAssetResourceManager.default().cancelDataRequest(id)
} else {
armIdleDeadline(ref, after: kBaseReadTimeoutSeconds - idle)
}
}
}
// Shared gate for the base readers: fetch the asset, classify the edit from its
// adjustment metadata, and pick the original resources. nil = positively nothing
// to recover (missing asset, not edited, or no readable original still). An
// unreadable plist throws instead that's "can't tell right now", and Dart
// defers the asset rather than uploading the edit standalone for good.
private static func originalsForEditedAsset(
_ assetId: String,
allowNetworkAccess: Bool
) async throws -> (still: PHAssetResource, video: PHAssetResource?)? {
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else {
return nil
}
let resources = PHAssetResource.assetResources(for: asset)
let state = await classifyEdit(resources: resources, allowNetworkAccess: allowNetworkAccess)
if state == .unknown {
throw PigeonError(
code: "unknownEditState",
message: "Could not read adjustment metadata for \(assetId)",
details: nil
)
}
guard state == .edited, let still = originalStillResource(resources) else {
return nil
}
return (still, originalPairedVideoResource(resources))
}
// Works out the edit state from Adjustments.plist only (never reads the photo).
// adjustmentRenderTypes is the signal: a real edit moves it off the baseline, while a
// plain capture, a Photographic Style, and a reverted edit all sit at the baseline. The
// editor id is NOT reliable: com.apple.camera authors both styles and some real edits
// (e.g. changing the Photographic Style after capture), so we key off the render types
// alone. Cleanup and object-removal write AdjustmentsSecondary.data, which we count as
// edited. unknown = couldn't read the plist (offloaded, no network).
private static func classifyEdit(resources: [PHAssetResource], allowNetworkAccess: Bool) async -> EditState {
if resources.contains(where: { $0.originalFilename == "AdjustmentsSecondary.data" }) {
return .edited
}
guard let adjRes = resources.first(where: { $0.originalFilename == "Adjustments.plist" }) else {
return .notEdited
}
guard let buf = await collectResourceData(adjRes, allowNetworkAccess: allowNetworkAccess),
let plist = try? PropertyListSerialization.propertyList(from: buf, options: [], format: nil) as? [String: Any]
else {
return .unknown
}
let renderTypes = (plist["adjustmentRenderTypes"] as? NSNumber)?.intValue
let isUserEdit = renderTypes != nil && renderTypes != kNoEditRenderTypes
return isUserEdit ? .edited : .notEdited
}
// The unedited original still, told apart from the edited "current" render by isCurrent.
// Prefer the non-current .photo; fall back to the .adjustmentBasePhoto flavor some
// creation-API / third-party-editor layouts use for the unaltered source (their .photo
// IS the edited render, so this must come before the bare .photo net); last, a lone
// .photo for single-resource assets or a failed isCurrent read.
private static func originalStillResource(_ resources: [PHAssetResource]) -> PHAssetResource? {
return resources.first(where: { $0.type == .photo && !$0.isCurrent })
?? resources.first(where: { $0.type == .adjustmentBasePhoto })
?? resources.first(where: { $0.type == .photo })
}
// The unedited original paired video, same isCurrent / adjustment-base ordering as the
// still. nil when the asset carries no paired video (not live, or Live turned off).
private static func originalPairedVideoResource(_ resources: [PHAssetResource]) -> PHAssetResource? {
return resources.first(where: { $0.type == .pairedVideo && !$0.isCurrent })
?? resources.first(where: { $0.type == .adjustmentBasePairedVideo })
?? resources.first(where: { $0.type == .pairedVideo })
}
private func streamBaseResource(
resource: PHAssetResource,
localId: String,
allowNetworkAccess: Bool
) async throws -> BaseResource {
let safeId = localId.replacingOccurrences(of: "/", with: "_")
let suffix = UTType(resource.uniformTypeIdentifier)?.preferredFilenameExtension ?? "bin"
// Library/Caches, not tmp: the chain can span launches and clearCache wipes
// tmp at the start of every upload run. Swept by clearEditBaseCache instead.
let cacheRoot =
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let tempDir = cacheRoot.appendingPathComponent("immich_base", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let unique = UUID().uuidString.prefix(8)
let tempUrl = tempDir.appendingPathComponent("\(safeId)_\(unique)_base.\(suffix)")
// Write the resource to disk and hash it chunk by chunk, so a big original (e.g.
// ProRAW) never sits fully in memory on the upload thread.
FileManager.default.createFile(atPath: tempUrl.path, contents: nil)
guard let handle = try? FileHandle(forWritingTo: tempUrl) else {
try? FileManager.default.removeItem(at: tempUrl)
throw PigeonError(
code: "baseResourceWriteFailed",
message: "Failed to open temp file for base resource \(localId)",
details: nil
)
}
var hasher = Insecure.SHA1()
let options = PHAssetResourceRequestOptions()
options.isNetworkAccessAllowed = allowNetworkAccess
// Deadline + cancellation so a stalled iCloud read can't hang the backup forever;
// a write failure also cancels right away instead of draining the download for nothing.
let requestRef = ResourceRequestRef()
let succeeded = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
var writeFailed = false
requestRef.id = PHAssetResourceManager.default().requestData(
for: resource,
options: options,
dataReceivedHandler: { chunk in
requestRef.lastActivity = DispatchTime.now()
if writeFailed { return }
do {
try handle.write(contentsOf: chunk)
hasher.update(data: chunk)
} catch {
writeFailed = true
if let id = requestRef.id {
PHAssetResourceManager.default().cancelDataRequest(id)
}
}
},
completionHandler: { error in
requestRef.id = nil
continuation.resume(returning: error == nil && !writeFailed)
}
)
Self.armIdleDeadline(requestRef)
}
try? handle.close()
guard succeeded else {
try? FileManager.default.removeItem(at: tempUrl)
throw PigeonError(
code: "baseResourceReadFailed",
message: "Failed to read base resource for \(localId)",
details: nil
)
}
let sha1 = Data(hasher.finalize()).base64EncodedString()
return BaseResource(path: tempUrl.path, sha1: sha1)
}
private static func collectResourceData(
_ resource: PHAssetResource,
allowNetworkAccess: Bool
) async -> Data? {
let options = PHAssetResourceRequestOptions()
options.isNetworkAccessAllowed = allowNetworkAccess
var buffer = Data()
let requestRef = ResourceRequestRef()
return await withCheckedContinuation { (continuation: CheckedContinuation<Data?, Never>) in
requestRef.id = PHAssetResourceManager.default().requestData(
for: resource,
options: options,
dataReceivedHandler: { data in
requestRef.lastActivity = DispatchTime.now()
buffer.append(data)
},
completionHandler: { error in
requestRef.id = nil
continuation.resume(returning: error == nil ? buffer : nil)
}
)
armIdleDeadline(requestRef)
}
}
}
@@ -27,9 +27,7 @@ extension PHAsset {
adjustmentTime: adjustmentTimestamp,
latitude: location?.coordinate.latitude,
longitude: location?.coordinate.longitude,
playbackStyle: platformPlaybackStyle,
burstId: burstIdentifier,
isBurstRepresentative: representsBurst
playbackStyle: platformPlaybackStyle
)
}
-16
View File
@@ -20,22 +20,6 @@ const String kSecuredPinCode = "secured_pin_code";
const String kManualUploadGroup = 'manual_upload_group';
const String kBackupGroup = 'backup_group';
const String kBackupLivePhotoGroup = 'backup_live_photo_group';
const String kBackupEditPairGroup = 'backup_edit_pair_group';
// Upload multipart 'visibility' value for motion videos (server AssetVisibility.Hidden)
// so they never flash onto the timeline before their still links them.
const String kHiddenVisibility = 'hidden';
// Server's 400 message when stackParentId points at a trashed/deleted asset
// (asset-media.service.ts). Matching it clears the stale prior stamps so the
// next backup cycle re-resolves instead of looping on the same dead id.
const String kDeadStackParentError = 'Cannot stack onto a trashed or missing asset';
// Multipart fields that stack a burst frame under its representative without
// letting it steal the cover (server keepPrimary, asset-media.service.ts).
// Empty when there's no anchor yet (rep-less group → standalone upload).
Map<String, String> burstStackFields(String? anchorRemoteId) =>
anchorRemoteId != null ? {'stackParentId': anchorRemoteId, 'keepPrimary': 'true'} : const {};
const String kDownloadGroupImage = 'group_image';
const String kDownloadGroupVideo = 'group_video';
const String kDownloadGroupLivePhoto = 'group_livephoto';
+1
View File
@@ -5,6 +5,7 @@ const Map<String, Locale> locales = {
'English (en)': Locale('en'),
// Additional locales
'Arabic (ar)': Locale('ar'),
'Basque (eu)': Locale('eu'),
'Bosnian (bl)': Locale('bn'),
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
'Bulgarian (bg)': Locale('bg'),
@@ -12,19 +12,6 @@ class LocalAsset extends BaseAsset {
final double? latitude;
final double? longitude;
// Remote id of this asset's previous upload; used to stack a new edit under it.
final String? priorRemoteId;
// Local checksum at the last sync action; lets backup skip an already-handled
// local whose current render hashes fresh (the iOS revert case).
final String? syncedChecksum;
// iOS burst grouping. burstId = PHAsset.burstIdentifier (null for non-burst).
// isBurstRepresentative = the auto-picked lead frame (timeline tile + stack
// anchor).
final String? burstId;
final bool isBurstRepresentative;
const LocalAsset({
required this.id,
String? remoteId,
@@ -45,10 +32,6 @@ class LocalAsset extends BaseAsset {
this.latitude,
this.longitude,
required super.isEdited,
this.priorRemoteId,
this.syncedChecksum,
this.burstId,
this.isBurstRepresentative = false,
}) : remoteAssetId = remoteId;
@override
@@ -137,10 +120,6 @@ class LocalAsset extends BaseAsset {
double? latitude,
double? longitude,
bool? isEdited,
String? priorRemoteId,
String? syncedChecksum,
String? burstId,
bool? isBurstRepresentative,
}) {
return LocalAsset(
id: id ?? this.id,
@@ -161,10 +140,6 @@ class LocalAsset extends BaseAsset {
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
isEdited: isEdited ?? this.isEdited,
priorRemoteId: priorRemoteId ?? this.priorRemoteId,
syncedChecksum: syncedChecksum ?? this.syncedChecksum,
burstId: burstId ?? this.burstId,
isBurstRepresentative: isBurstRepresentative ?? this.isBurstRepresentative,
);
}
}
@@ -13,11 +13,6 @@ class RemoteAsset extends BaseAsset {
final DateTime? uploadedAt;
final DateTime? deletedAt;
// The linked local's current checksum. Differs from [checksum] when the link
// came via priorRemoteId (the local re-encoded on device, e.g. a revert); local
// renders are cache-keyed by this so on-device changes aren't shown stale.
final String? localChecksum;
const RemoteAsset({
required this.id,
String? localId,
@@ -38,7 +33,6 @@ class RemoteAsset extends BaseAsset {
this.stackId,
required super.isEdited,
this.deletedAt,
this.localChecksum,
}) : localAssetId = localId;
@override
@@ -97,8 +91,7 @@ class RemoteAsset extends BaseAsset {
visibility == other.visibility &&
stackId == other.stackId &&
uploadedAt == other.uploadedAt &&
deletedAt == other.deletedAt &&
localChecksum == other.localChecksum;
deletedAt == other.deletedAt;
}
@override
@@ -111,8 +104,7 @@ class RemoteAsset extends BaseAsset {
visibility.hashCode ^
stackId.hashCode ^
uploadedAt.hashCode ^
deletedAt.hashCode ^
localChecksum.hashCode;
deletedAt.hashCode;
RemoteAsset copyWith({
String? id,
@@ -134,7 +126,6 @@ class RemoteAsset extends BaseAsset {
String? stackId,
bool? isEdited,
DateTime? deletedAt,
String? localChecksum,
}) {
return RemoteAsset(
id: id ?? this.id,
@@ -156,7 +147,6 @@ class RemoteAsset extends BaseAsset {
stackId: stackId ?? this.stackId,
isEdited: isEdited ?? this.isEdited,
deletedAt: deletedAt ?? this.deletedAt,
localChecksum: localChecksum ?? this.localChecksum,
);
}
}
@@ -184,7 +174,6 @@ class RemoteAssetExif extends RemoteAsset {
super.livePhotoVideoId,
super.stackId,
super.isEdited = false,
super.localChecksum,
this.exifInfo = const ExifInfo(),
});
@@ -223,7 +212,6 @@ class RemoteAssetExif extends RemoteAsset {
String? livePhotoVideoId,
String? stackId,
bool? isEdited,
String? localChecksum,
ExifInfo? exifInfo,
}) {
return RemoteAssetExif(
@@ -246,7 +234,6 @@ class RemoteAssetExif extends RemoteAsset {
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
stackId: stackId ?? this.stackId,
isEdited: isEdited ?? this.isEdited,
localChecksum: localChecksum ?? this.localChecksum,
exifInfo: exifInfo ?? this.exifInfo, // Use the new parameter
);
}
@@ -268,7 +268,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
return _ref?.read(driftBackupProvider.notifier).startBackupWithURLSession(currentUser.id);
}
await _ref
return _ref
?.read(foregroundUploadServiceProvider)
.uploadCandidates(currentUser.id, _cancellationToken, useSequentialUpload: true);
},
@@ -1,95 +0,0 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:logging/logging.dart';
/// Handles an edit that was reverted in Photos. The local was uploaded as an edit
/// before but isn't edited now, so flip the stack primary back to the original (via
/// prior_remote_id) and mark it handled so we don't re-upload the reverted render.
/// Nothing is trashed; all the edits stay in the stack.
class EditRevertService {
final NativeSyncApi _nativeSyncApi;
final DriftStackRepository _stackRepository;
final DriftLocalAssetRepository _localAssetRepository;
final AssetApiRepository _assetApiRepository;
final _log = Logger('EditRevertService');
EditRevertService({
required this._nativeSyncApi,
required this._stackRepository,
required this._localAssetRepository,
required this._assetApiRepository,
});
/// Returns the remote id the stack cover was flipped back to when the asset
/// was a revert and was handled (caller skips the upload and can report that
/// id); null to fall through to the normal upload path.
Future<String?> tryHandleRevert(LocalAsset asset) async {
if (asset.priorRemoteId == null) {
return null;
}
// Only "not edited" is a revert. `edited` is a fresh edit, so let the pair flow
// take it. `unknown` means we couldn't read the adjustment (offloaded to iCloud,
// network off); bail there too instead of mistaking an unreadable edit for a
// revert and flipping the stack. Network off keeps this a cheap offline read.
try {
final editState = await _nativeSyncApi
.getEditState(asset.id, allowNetworkAccess: false)
.timeout(const Duration(seconds: 30));
if (editState != EditState.notEdited) {
return null;
}
} catch (error, stack) {
_log.warning("edit-state probe failed for ${asset.id}", error, stack);
return null;
}
// It's a revert. Styled photos hit this path because iOS re-encodes the revert to
// fresh bytes, so it looks like a new backup candidate and reaches upload.
// Non-styled reverts hash back to the base instead, aren't candidates, and get
// flipped at hash time in HashService._reconcileReverts. Fresh bytes match nothing
// remote, so flip by structure: prior_remote_id is the current primary (the latest
// edit), flip it back to the base.
final String stackId;
final String baseId;
try {
final foundStack = await _stackRepository.findStackIdByRemoteId(asset.priorRemoteId!);
if (foundStack == null) {
return null;
}
final base = await _stackRepository.findStackBaseId(foundStack, excludeId: asset.priorRemoteId!);
if (base == null) {
return null;
}
stackId = foundStack;
baseId = base;
} catch (error, stack) {
_log.warning("revert stack lookup failed for ${asset.id}", error, stack);
return null;
}
try {
await _assetApiRepository.setStackPrimary(stackId, baseId);
} catch (error, stack) {
_log.warning("revert primary flip failed for ${asset.id}", error, stack);
return null;
}
// The server flip is what makes the revert handled. If the local writes fail,
// falling through would upload the reverted render as a brand-new edit the
// opposite of the user's action — so log and let checkpoint sync heal local state.
try {
await _stackRepository.setPrimary(stackId, baseId);
await _localAssetRepository.markSynced(asset.id, priorRemoteId: baseId, syncedChecksum: asset.checksum);
} catch (error, stack) {
_log.warning("revert local reconcile failed for ${asset.id}", error, stack);
}
return baseId;
}
}
@@ -7,10 +7,8 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:logging/logging.dart';
const String _kHashCancelledCode = "HASH_CANCELLED";
@@ -22,8 +20,6 @@ class HashService {
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final NativeSyncApi _nativeSyncApi;
final Completer<void>? _cancellation;
final DriftStackRepository _stackRepository;
final AssetApiRepository _assetApiRepository;
final _log = Logger('HashService');
HashService({
@@ -32,8 +28,6 @@ class HashService {
required this._trashedLocalAssetRepository,
required this._nativeSyncApi,
this._cancellation,
required this._stackRepository,
required this._assetApiRepository,
int? batchSize,
}) : _batchSize = batchSize ?? kBatchHashFileLimit {
// Stop the in-flight native hash call promptly on cancellation; the loops
@@ -72,17 +66,6 @@ class HashService {
await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true);
}
}
// Revert reconcile for non-styled photos: the reverted edit hashes back to the
// original's exact bytes, which are already the stack base, so it's not a backup
// candidate and never reaches upload. Flip the primary here. Styled photos
// re-encode to fresh bytes and get flipped on the upload path instead
// (EditRevertService.tryHandleRevert). Runs every cycle, not just when something
// hashed: a flip that failed (offline at hash time) has no second hash to ride,
// and the stack-driven target query is cheap and self-limiting.
if (CurrentPlatform.isIOS && !isCancelled) {
await _reconcileReverts();
}
} on PlatformException catch (e) {
if (e.code == _kHashCancelledCode) {
_log.warning("Hashing cancelled by platform");
@@ -160,30 +143,4 @@ class HashService {
await _localAssetRepository.updateHashes(hashed);
}
}
Future<void> _reconcileReverts() async {
final List<StackReconcileTarget> targets;
try {
targets = await _stackRepository.findRevertReconcileTargets();
} catch (error, stack) {
_log.warning("findRevertReconcileTargets failed", error, stack);
return;
}
for (final target in targets) {
try {
await _assetApiRepository.setStackPrimary(target.stackId, target.newPrimaryId);
await _stackRepository.setPrimary(target.stackId, target.newPrimaryId);
// Roll priorRemoteId forward to the matched member (now the primary) so a
// later edit stacks onto THAT (the current render), not the old edit.
await _localAssetRepository.markSynced(
target.localAssetId,
priorRemoteId: target.newPrimaryId,
syncedChecksum: target.localAssetChecksum,
);
} catch (error, stack) {
_log.warning("revert reconcile flip failed for stack ${target.stackId}", error, stack);
}
}
}
}
@@ -469,8 +469,6 @@ extension PlatformToLocalAsset on PlatformAsset {
latitude: latitude,
longitude: longitude,
isEdited: false,
burstId: burstId,
isBurstRepresentative: isBurstRepresentative,
);
}
@@ -360,7 +360,7 @@ class SyncStreamService {
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch', fromWebsocket: true);
await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
@@ -403,7 +403,7 @@ class SyncStreamService {
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV2(assets, debugLabel: 'websocket-batch', fromWebsocket: true);
await _syncStreamRepository.updateAssetsV2(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
@@ -444,8 +444,9 @@ class SyncStreamService {
.toList();
}
await _syncStreamRepository.updateAssetsV1([asset], debugLabel: 'websocket-edit', fromWebsocket: true);
await _syncStreamRepository.updateAssetsV1([asset], debugLabel: 'websocket-edit');
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
await _refreshAssetOcrAndFaces(asset.id);
_logger.info(
'Successfully processed AssetEditReadyV1 event for asset ${asset.id} with ${assetEdits.length} edits',
@@ -482,8 +483,9 @@ class SyncStreamService {
.whereType<SyncAssetEditV1>()
.toList();
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit', fromWebsocket: true);
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
await _refreshAssetOcrAndFaces(asset.id);
_logger.info(
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
@@ -493,6 +495,22 @@ class SyncStreamService {
}
}
Future<void> _refreshAssetOcrAndFaces(String assetId) async {
try {
final ocr = await _api.assetsApi.getAssetOcr(assetId);
await _syncStreamRepository.replaceAssetOcr(assetId, ocr ?? const []);
} catch (error, stackTrace) {
_logger.severe("Error refreshing OCR for asset $assetId", error, stackTrace);
}
try {
final faces = await _api.facesApi.getFaces(assetId);
await _syncStreamRepository.replaceAssetFaces(assetId, faces ?? const []);
} catch (error, stackTrace) {
_logger.severe("Error refreshing faces for asset $assetId", error, stackTrace);
}
}
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
if (remoteIds.isEmpty) {
return Future.value();
@@ -160,22 +160,6 @@ class BackgroundSyncManager {
});
}
/// Runs a remote sync guaranteed to observe changes up to now. [syncRemote]
/// joins an in-flight sync whose snapshot can pre-date a just-received change
/// (e.g. a stack update) and miss it, so wait for any in-flight sync to finish
/// first, then run a fresh one.
Future<void> runFreshRemoteSync() async {
final inflight = _syncTask;
if (inflight != null) {
try {
await inflight.future;
} catch (_) {
// The in-flight sync's outcome doesn't matter; we only need a fresh one after it.
}
}
await syncRemote();
}
Future<void> syncWebsocketBatchV1(List<dynamic> batchData) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
@@ -7,8 +7,6 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_prior_remote_id ON local_asset_entity (prior_remote_id)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_burst_id ON local_asset_entity (burst_id)')
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const LocalAssetEntity();
@@ -30,21 +28,6 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
IntColumn get playbackStyle => intEnum<AssetPlaybackStyle>().withDefault(const Constant(0))();
// remote id of the previous upload (iOS edit-pair stacking)
TextColumn get priorRemoteId => text().nullable()();
// local checksum at the last sync action. Lets the backup query skip a local
// whose current hash matches nothing remote but is still "handled": the iOS
// revert case, where the reverted render hashes fresh but is already reconciled.
TextColumn get syncedChecksum => text().nullable()();
// iOS burst grouping. burstId = PHAsset.burstIdentifier (null for non-burst).
// isBurstRepresentative = the auto-picked lead frame at detection; the rep is
// the timeline tile and the stack anchor. Both re-sync on every delta, so a
// Photos re-pick that moves the rep flag is reflected.
TextColumn get burstId => text().nullable()();
BoolColumn get isBurstRepresentative => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}
@@ -69,9 +52,5 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
longitude: longitude,
cloudId: iCloudId,
isEdited: false,
priorRemoteId: priorRemoteId,
syncedChecksum: syncedChecksum,
burstId: burstId,
isBurstRepresentative: isBurstRepresentative,
);
}
@@ -26,10 +26,6 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
i0.Value<double?> latitude,
i0.Value<double?> longitude,
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
i0.Value<String?> priorRemoteId,
i0.Value<String?> syncedChecksum,
i0.Value<String?> burstId,
i0.Value<bool> isBurstRepresentative,
});
typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i1.LocalAssetEntityCompanion Function({
@@ -49,10 +45,6 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<double?> latitude,
i0.Value<double?> longitude,
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
i0.Value<String?> priorRemoteId,
i0.Value<String?> syncedChecksum,
i0.Value<String?> burstId,
i0.Value<bool> isBurstRepresentative,
});
class $$LocalAssetEntityTableFilterComposer
@@ -149,26 +141,6 @@ class $$LocalAssetEntityTableFilterComposer
column: $table.playbackStyle,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<String> get priorRemoteId => $composableBuilder(
column: $table.priorRemoteId,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get syncedChecksum => $composableBuilder(
column: $table.syncedChecksum,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get burstId => $composableBuilder(
column: $table.burstId,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<bool> get isBurstRepresentative => $composableBuilder(
column: $table.isBurstRepresentative,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$LocalAssetEntityTableOrderingComposer
@@ -259,26 +231,6 @@ class $$LocalAssetEntityTableOrderingComposer
column: $table.playbackStyle,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get priorRemoteId => $composableBuilder(
column: $table.priorRemoteId,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get syncedChecksum => $composableBuilder(
column: $table.syncedChecksum,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get burstId => $composableBuilder(
column: $table.burstId,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<bool> get isBurstRepresentative => $composableBuilder(
column: $table.isBurstRepresentative,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$LocalAssetEntityTableAnnotationComposer
@@ -348,24 +300,6 @@ class $$LocalAssetEntityTableAnnotationComposer
column: $table.playbackStyle,
builder: (column) => column,
);
i0.GeneratedColumn<String> get priorRemoteId => $composableBuilder(
column: $table.priorRemoteId,
builder: (column) => column,
);
i0.GeneratedColumn<String> get syncedChecksum => $composableBuilder(
column: $table.syncedChecksum,
builder: (column) => column,
);
i0.GeneratedColumn<String> get burstId =>
$composableBuilder(column: $table.burstId, builder: (column) => column);
i0.GeneratedColumn<bool> get isBurstRepresentative => $composableBuilder(
column: $table.isBurstRepresentative,
builder: (column) => column,
);
}
class $$LocalAssetEntityTableTableManager
@@ -425,10 +359,6 @@ class $$LocalAssetEntityTableTableManager
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
const i0.Value.absent(),
i0.Value<String?> priorRemoteId = const i0.Value.absent(),
i0.Value<String?> syncedChecksum = const i0.Value.absent(),
i0.Value<String?> burstId = const i0.Value.absent(),
i0.Value<bool> isBurstRepresentative = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion(
name: name,
type: type,
@@ -446,10 +376,6 @@ class $$LocalAssetEntityTableTableManager
latitude: latitude,
longitude: longitude,
playbackStyle: playbackStyle,
priorRemoteId: priorRemoteId,
syncedChecksum: syncedChecksum,
burstId: burstId,
isBurstRepresentative: isBurstRepresentative,
),
createCompanionCallback:
({
@@ -470,10 +396,6 @@ class $$LocalAssetEntityTableTableManager
i0.Value<double?> longitude = const i0.Value.absent(),
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
const i0.Value.absent(),
i0.Value<String?> priorRemoteId = const i0.Value.absent(),
i0.Value<String?> syncedChecksum = const i0.Value.absent(),
i0.Value<String?> burstId = const i0.Value.absent(),
i0.Value<bool> isBurstRepresentative = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion.insert(
name: name,
type: type,
@@ -491,10 +413,6 @@ class $$LocalAssetEntityTableTableManager
latitude: latitude,
longitude: longitude,
playbackStyle: playbackStyle,
priorRemoteId: priorRemoteId,
syncedChecksum: syncedChecksum,
burstId: burstId,
isBurstRepresentative: isBurstRepresentative,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -719,54 +637,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
).withConverter<i2.AssetPlaybackStyle>(
i1.$LocalAssetEntityTable.$converterplaybackStyle,
);
static const i0.VerificationMeta _priorRemoteIdMeta =
const i0.VerificationMeta('priorRemoteId');
@override
late final i0.GeneratedColumn<String> priorRemoteId =
i0.GeneratedColumn<String>(
'prior_remote_id',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _syncedChecksumMeta =
const i0.VerificationMeta('syncedChecksum');
@override
late final i0.GeneratedColumn<String> syncedChecksum =
i0.GeneratedColumn<String>(
'synced_checksum',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _burstIdMeta = const i0.VerificationMeta(
'burstId',
);
@override
late final i0.GeneratedColumn<String> burstId = i0.GeneratedColumn<String>(
'burst_id',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _isBurstRepresentativeMeta =
const i0.VerificationMeta('isBurstRepresentative');
@override
late final i0.GeneratedColumn<bool> isBurstRepresentative =
i0.GeneratedColumn<bool>(
'is_burst_representative',
aliasedName,
false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_burst_representative" IN (0, 1))',
),
defaultValue: const i4.Constant(false),
);
@override
List<i0.GeneratedColumn> get $columns => [
name,
@@ -785,10 +655,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
latitude,
longitude,
playbackStyle,
priorRemoteId,
syncedChecksum,
burstId,
isBurstRepresentative,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -893,39 +759,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta),
);
}
if (data.containsKey('prior_remote_id')) {
context.handle(
_priorRemoteIdMeta,
priorRemoteId.isAcceptableOrUnknown(
data['prior_remote_id']!,
_priorRemoteIdMeta,
),
);
}
if (data.containsKey('synced_checksum')) {
context.handle(
_syncedChecksumMeta,
syncedChecksum.isAcceptableOrUnknown(
data['synced_checksum']!,
_syncedChecksumMeta,
),
);
}
if (data.containsKey('burst_id')) {
context.handle(
_burstIdMeta,
burstId.isAcceptableOrUnknown(data['burst_id']!, _burstIdMeta),
);
}
if (data.containsKey('is_burst_representative')) {
context.handle(
_isBurstRepresentativeMeta,
isBurstRepresentative.isAcceptableOrUnknown(
data['is_burst_representative']!,
_isBurstRepresentativeMeta,
),
);
}
return context;
}
@@ -1006,22 +839,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
data['${effectivePrefix}playback_style'],
)!,
),
priorRemoteId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}prior_remote_id'],
),
syncedChecksum: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}synced_checksum'],
),
burstId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}burst_id'],
),
isBurstRepresentative: attachedDatabase.typeMapping.read(
i0.DriftSqlType.bool,
data['${effectivePrefix}is_burst_representative'],
)!,
);
}
@@ -1060,10 +877,6 @@ class LocalAssetEntityData extends i0.DataClass
final double? latitude;
final double? longitude;
final i2.AssetPlaybackStyle playbackStyle;
final String? priorRemoteId;
final String? syncedChecksum;
final String? burstId;
final bool isBurstRepresentative;
const LocalAssetEntityData({
required this.name,
required this.type,
@@ -1081,10 +894,6 @@ class LocalAssetEntityData extends i0.DataClass
this.latitude,
this.longitude,
required this.playbackStyle,
this.priorRemoteId,
this.syncedChecksum,
this.burstId,
required this.isBurstRepresentative,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -1129,16 +938,6 @@ class LocalAssetEntityData extends i0.DataClass
i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql(playbackStyle),
);
}
if (!nullToAbsent || priorRemoteId != null) {
map['prior_remote_id'] = i0.Variable<String>(priorRemoteId);
}
if (!nullToAbsent || syncedChecksum != null) {
map['synced_checksum'] = i0.Variable<String>(syncedChecksum);
}
if (!nullToAbsent || burstId != null) {
map['burst_id'] = i0.Variable<String>(burstId);
}
map['is_burst_representative'] = i0.Variable<bool>(isBurstRepresentative);
return map;
}
@@ -1168,12 +967,6 @@ class LocalAssetEntityData extends i0.DataClass
playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromJson(
serializer.fromJson<int>(json['playbackStyle']),
),
priorRemoteId: serializer.fromJson<String?>(json['priorRemoteId']),
syncedChecksum: serializer.fromJson<String?>(json['syncedChecksum']),
burstId: serializer.fromJson<String?>(json['burstId']),
isBurstRepresentative: serializer.fromJson<bool>(
json['isBurstRepresentative'],
),
);
}
@override
@@ -1200,10 +993,6 @@ class LocalAssetEntityData extends i0.DataClass
'playbackStyle': serializer.toJson<int>(
i1.$LocalAssetEntityTable.$converterplaybackStyle.toJson(playbackStyle),
),
'priorRemoteId': serializer.toJson<String?>(priorRemoteId),
'syncedChecksum': serializer.toJson<String?>(syncedChecksum),
'burstId': serializer.toJson<String?>(burstId),
'isBurstRepresentative': serializer.toJson<bool>(isBurstRepresentative),
};
}
@@ -1224,10 +1013,6 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
i2.AssetPlaybackStyle? playbackStyle,
i0.Value<String?> priorRemoteId = const i0.Value.absent(),
i0.Value<String?> syncedChecksum = const i0.Value.absent(),
i0.Value<String?> burstId = const i0.Value.absent(),
bool? isBurstRepresentative,
}) => i1.LocalAssetEntityData(
name: name ?? this.name,
type: type ?? this.type,
@@ -1247,14 +1032,6 @@ class LocalAssetEntityData extends i0.DataClass
latitude: latitude.present ? latitude.value : this.latitude,
longitude: longitude.present ? longitude.value : this.longitude,
playbackStyle: playbackStyle ?? this.playbackStyle,
priorRemoteId: priorRemoteId.present
? priorRemoteId.value
: this.priorRemoteId,
syncedChecksum: syncedChecksum.present
? syncedChecksum.value
: this.syncedChecksum,
burstId: burstId.present ? burstId.value : this.burstId,
isBurstRepresentative: isBurstRepresentative ?? this.isBurstRepresentative,
);
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData(
@@ -1284,16 +1061,6 @@ class LocalAssetEntityData extends i0.DataClass
playbackStyle: data.playbackStyle.present
? data.playbackStyle.value
: this.playbackStyle,
priorRemoteId: data.priorRemoteId.present
? data.priorRemoteId.value
: this.priorRemoteId,
syncedChecksum: data.syncedChecksum.present
? data.syncedChecksum.value
: this.syncedChecksum,
burstId: data.burstId.present ? data.burstId.value : this.burstId,
isBurstRepresentative: data.isBurstRepresentative.present
? data.isBurstRepresentative.value
: this.isBurstRepresentative,
);
}
@@ -1315,11 +1082,7 @@ class LocalAssetEntityData extends i0.DataClass
..write('adjustmentTime: $adjustmentTime, ')
..write('latitude: $latitude, ')
..write('longitude: $longitude, ')
..write('playbackStyle: $playbackStyle, ')
..write('priorRemoteId: $priorRemoteId, ')
..write('syncedChecksum: $syncedChecksum, ')
..write('burstId: $burstId, ')
..write('isBurstRepresentative: $isBurstRepresentative')
..write('playbackStyle: $playbackStyle')
..write(')'))
.toString();
}
@@ -1342,10 +1105,6 @@ class LocalAssetEntityData extends i0.DataClass
latitude,
longitude,
playbackStyle,
priorRemoteId,
syncedChecksum,
burstId,
isBurstRepresentative,
);
@override
bool operator ==(Object other) =>
@@ -1366,11 +1125,7 @@ class LocalAssetEntityData extends i0.DataClass
other.adjustmentTime == this.adjustmentTime &&
other.latitude == this.latitude &&
other.longitude == this.longitude &&
other.playbackStyle == this.playbackStyle &&
other.priorRemoteId == this.priorRemoteId &&
other.syncedChecksum == this.syncedChecksum &&
other.burstId == this.burstId &&
other.isBurstRepresentative == this.isBurstRepresentative);
other.playbackStyle == this.playbackStyle);
}
class LocalAssetEntityCompanion
@@ -1391,10 +1146,6 @@ class LocalAssetEntityCompanion
final i0.Value<double?> latitude;
final i0.Value<double?> longitude;
final i0.Value<i2.AssetPlaybackStyle> playbackStyle;
final i0.Value<String?> priorRemoteId;
final i0.Value<String?> syncedChecksum;
final i0.Value<String?> burstId;
final i0.Value<bool> isBurstRepresentative;
const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(),
@@ -1412,10 +1163,6 @@ class LocalAssetEntityCompanion
this.latitude = const i0.Value.absent(),
this.longitude = const i0.Value.absent(),
this.playbackStyle = const i0.Value.absent(),
this.priorRemoteId = const i0.Value.absent(),
this.syncedChecksum = const i0.Value.absent(),
this.burstId = const i0.Value.absent(),
this.isBurstRepresentative = const i0.Value.absent(),
});
LocalAssetEntityCompanion.insert({
required String name,
@@ -1434,10 +1181,6 @@ class LocalAssetEntityCompanion
this.latitude = const i0.Value.absent(),
this.longitude = const i0.Value.absent(),
this.playbackStyle = const i0.Value.absent(),
this.priorRemoteId = const i0.Value.absent(),
this.syncedChecksum = const i0.Value.absent(),
this.burstId = const i0.Value.absent(),
this.isBurstRepresentative = const i0.Value.absent(),
}) : name = i0.Value(name),
type = i0.Value(type),
id = i0.Value(id);
@@ -1458,10 +1201,6 @@ class LocalAssetEntityCompanion
i0.Expression<double>? latitude,
i0.Expression<double>? longitude,
i0.Expression<int>? playbackStyle,
i0.Expression<String>? priorRemoteId,
i0.Expression<String>? syncedChecksum,
i0.Expression<String>? burstId,
i0.Expression<bool>? isBurstRepresentative,
}) {
return i0.RawValuesInsertable({
if (name != null) 'name': name,
@@ -1480,11 +1219,6 @@ class LocalAssetEntityCompanion
if (latitude != null) 'latitude': latitude,
if (longitude != null) 'longitude': longitude,
if (playbackStyle != null) 'playback_style': playbackStyle,
if (priorRemoteId != null) 'prior_remote_id': priorRemoteId,
if (syncedChecksum != null) 'synced_checksum': syncedChecksum,
if (burstId != null) 'burst_id': burstId,
if (isBurstRepresentative != null)
'is_burst_representative': isBurstRepresentative,
});
}
@@ -1505,10 +1239,6 @@ class LocalAssetEntityCompanion
i0.Value<double?>? latitude,
i0.Value<double?>? longitude,
i0.Value<i2.AssetPlaybackStyle>? playbackStyle,
i0.Value<String?>? priorRemoteId,
i0.Value<String?>? syncedChecksum,
i0.Value<String?>? burstId,
i0.Value<bool>? isBurstRepresentative,
}) {
return i1.LocalAssetEntityCompanion(
name: name ?? this.name,
@@ -1527,11 +1257,6 @@ class LocalAssetEntityCompanion
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
playbackStyle: playbackStyle ?? this.playbackStyle,
priorRemoteId: priorRemoteId ?? this.priorRemoteId,
syncedChecksum: syncedChecksum ?? this.syncedChecksum,
burstId: burstId ?? this.burstId,
isBurstRepresentative:
isBurstRepresentative ?? this.isBurstRepresentative,
);
}
@@ -1592,20 +1317,6 @@ class LocalAssetEntityCompanion
),
);
}
if (priorRemoteId.present) {
map['prior_remote_id'] = i0.Variable<String>(priorRemoteId.value);
}
if (syncedChecksum.present) {
map['synced_checksum'] = i0.Variable<String>(syncedChecksum.value);
}
if (burstId.present) {
map['burst_id'] = i0.Variable<String>(burstId.value);
}
if (isBurstRepresentative.present) {
map['is_burst_representative'] = i0.Variable<bool>(
isBurstRepresentative.value,
);
}
return map;
}
@@ -1627,11 +1338,7 @@ class LocalAssetEntityCompanion
..write('adjustmentTime: $adjustmentTime, ')
..write('latitude: $latitude, ')
..write('longitude: $longitude, ')
..write('playbackStyle: $playbackStyle, ')
..write('priorRemoteId: $priorRemoteId, ')
..write('syncedChecksum: $syncedChecksum, ')
..write('burstId: $burstId, ')
..write('isBurstRepresentative: $isBurstRepresentative')
..write('playbackStyle: $playbackStyle')
..write(')'))
.toString();
}
@@ -1645,11 +1352,3 @@ i0.Index get idxLocalAssetCreatedAt => i0.Index(
'idx_local_asset_created_at',
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
);
i0.Index get idxLocalAssetPriorRemoteId => i0.Index(
'idx_local_asset_prior_remote_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_prior_remote_id ON local_asset_entity (prior_remote_id)',
);
i0.Index get idxLocalAssetBurstId => i0.Index(
'idx_local_asset_burst_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_burst_id ON local_asset_entity (burst_id)',
);
@@ -7,13 +7,7 @@ import 'local_album_asset.entity.dart';
mergedAsset:
SELECT
rae.id as remote_id,
-- local_id links a remote to its on-device copy, normally by checksum. A reverted iOS
-- edit re-encodes to fresh bytes so the checksum no longer matches, but its
-- prior_remote_id still points at this remote, so fall back to that.
COALESCE(
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1),
(SELECT lae.id FROM local_asset_entity lae WHERE lae.prior_remote_id = rae.id LIMIT 1)
) as local_id,
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
rae.name,
rae."type",
rae.created_at as created_at,
@@ -24,12 +18,6 @@ SELECT
rae.is_favorite,
rae.thumb_hash,
rae.checksum,
-- the linked local's current checksum (same row local_id picks), so local
-- renders are cache-keyed by the bytes on device, not the server value.
COALESCE(
(SELECT lae.checksum FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1),
(SELECT lae.checksum FROM local_asset_entity lae WHERE lae.prior_remote_id = rae.id LIMIT 1)
) as local_checksum,
rae.owner_id,
rae.live_photo_video_id,
0 as orientation,
@@ -53,9 +41,6 @@ WHERE
rae.stack_id IS NULL
OR rae.id = se.primary_asset_id
)
-- iOS burst: hide non-representative members only in the pre-stack-sync window;
-- once stack_id is set the rae.id = se.primary_asset_id rule above already hides them.
AND (rae.stack_id IS NOT NULL OR NOT EXISTS (SELECT 1 FROM local_asset_entity lae WHERE lae.checksum = rae.checksum AND lae.burst_id IS NOT NULL AND lae.is_burst_representative = 0))
UNION ALL
@@ -72,7 +57,6 @@ SELECT
lae.is_favorite,
NULL as thumb_hash,
lae.checksum,
lae.checksum as local_checksum,
NULL as owner_id,
NULL as live_photo_video_id,
lae.orientation,
@@ -99,23 +83,6 @@ AND NOT EXISTS (
INNER JOIN local_album_entity la on laa.album_id = la.id
WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded
)
-- iOS edit-in-progress / revert: if this local was already uploaded (its
-- prior_remote_id resolves to a remote row), hide the local tile so the remote
-- (the edit, or the flipped-back original) is the single source of truth. Kills
-- the transient 2-tile flicker and stops a reverted local from re-appearing.
-- A trashed prior still hides it — trashing on the server shouldn't pop the
-- photo back onto the local timeline; only a hard delete (row gone) does.
AND NOT EXISTS (
SELECT 1 FROM remote_asset_entity rae WHERE rae.id = lae.prior_remote_id AND rae.owner_id IN :user_ids
)
-- iOS burst: show only the representative as a local tile; other members still back up + stack.
-- A rep-less group (Keep Everything / re-pick) has no representative, so show every
-- frame as an individual instead of hiding the whole group.
AND (
lae.burst_id IS NULL
OR lae.is_burst_representative = 1
OR NOT EXISTS (SELECT 1 FROM local_asset_entity r WHERE r.burst_id = lae.burst_id AND r.is_burst_representative = 1)
)
ORDER BY created_at DESC
LIMIT $limit;
@@ -148,8 +115,6 @@ FROM
rae.stack_id IS NULL
OR rae.id = se.primary_asset_id
)
-- iOS burst: hide non-representative members only in the pre-stack-sync window (see mergedAsset)
AND (rae.stack_id IS NOT NULL OR NOT EXISTS (SELECT 1 FROM local_asset_entity lae WHERE lae.checksum = rae.checksum AND lae.burst_id IS NOT NULL AND lae.is_burst_representative = 0))
UNION ALL
SELECT
CASE
@@ -171,17 +136,6 @@ FROM
INNER JOIN local_album_entity la on laa.album_id = la.id
WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded
)
-- iOS edit-in-progress / revert: hide a local already represented by a remote
-- row (trashed included, same as the tile query above).
AND NOT EXISTS (
SELECT 1 FROM remote_asset_entity rae WHERE rae.id = lae.prior_remote_id AND rae.owner_id IN :user_ids
)
-- iOS burst: count only the representative; a rep-less group counts every frame (see mergedAsset)
AND (
lae.burst_id IS NULL
OR lae.is_burst_representative = 1
OR NOT EXISTS (SELECT 1 FROM local_asset_entity r WHERE r.burst_id = lae.burst_id AND r.is_burst_representative = 1)
)
)
GROUP BY bucket_date
ORDER BY bucket_date DESC;
+2 -5
View File
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
'SELECT rae.id AS remote_id, COALESCE((SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1), (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.prior_remote_id = rae.id LIMIT 1)) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, COALESCE((SELECT lae.checksum FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1), (SELECT lae.checksum FROM local_asset_entity AS lae WHERE lae.prior_remote_id = rae.id LIMIT 1)) AS local_checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)AND(rae.stack_id IS NOT NULL OR NOT EXISTS (SELECT 1 AS _c0 FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum AND lae.burst_id IS NOT NULL AND lae.is_burst_representative = 0))UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, lae.checksum AS local_checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) AND NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.id = lae.prior_remote_id AND rae.owner_id IN ($expandeduserIds)) AND(lae.burst_id IS NULL OR lae.is_burst_representative = 1 OR NOT EXISTS (SELECT 1 FROM local_asset_entity AS r WHERE r.burst_id = lae.burst_id AND r.is_burst_representative = 1))ORDER BY created_at DESC ${generatedlimit.sql}',
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables,
@@ -58,7 +58,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
isFavorite: row.read<bool>('is_favorite'),
thumbHash: row.readNullable<String>('thumb_hash'),
checksum: row.readNullable<String>('checksum'),
localChecksum: row.readNullable<String>('local_checksum'),
ownerId: row.readNullable<String>('owner_id'),
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'),
@@ -82,7 +81,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
final expandeduserIds = $expandVar($arrayStartIndex, userIds.length);
$arrayStartIndex += userIds.length;
return customSelect(
'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN COALESCE(STRFTIME(\'%Y-%m-%d\', rae.local_date_time), STRFTIME(\'%Y-%m-%d\', rae.created_at, \'localtime\')) WHEN ?1 = 1 THEN COALESCE(STRFTIME(\'%Y-%m\', rae.local_date_time), STRFTIME(\'%Y-%m\', rae.created_at, \'localtime\')) END AS bucket_date FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)AND(rae.stack_id IS NOT NULL OR NOT EXISTS (SELECT 1 AS _c0 FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum AND lae.burst_id IS NOT NULL AND lae.is_burst_representative = 0))UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) AND NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.id = lae.prior_remote_id AND rae.owner_id IN ($expandeduserIds)) AND(lae.burst_id IS NULL OR lae.is_burst_representative = 1 OR NOT EXISTS (SELECT 1 FROM local_asset_entity AS r WHERE r.burst_id = lae.burst_id AND r.is_burst_representative = 1))) GROUP BY bucket_date ORDER BY bucket_date DESC',
'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN COALESCE(STRFTIME(\'%Y-%m-%d\', rae.local_date_time), STRFTIME(\'%Y-%m-%d\', rae.created_at, \'localtime\')) WHEN ?1 = 1 THEN COALESCE(STRFTIME(\'%Y-%m\', rae.local_date_time), STRFTIME(\'%Y-%m\', rae.created_at, \'localtime\')) END AS bucket_date FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC',
variables: [
i0.Variable<int>(groupBy),
for (var $ in userIds) i0.Variable<String>($),
@@ -133,7 +132,6 @@ class MergedAssetResult {
final bool isFavorite;
final String? thumbHash;
final String? checksum;
final String? localChecksum;
final String? ownerId;
final String? livePhotoVideoId;
final int orientation;
@@ -158,7 +156,6 @@ class MergedAssetResult {
required this.isFavorite,
this.thumbHash,
this.checksum,
this.localChecksum,
this.ownerId,
this.livePhotoVideoId,
required this.orientation,
@@ -55,8 +55,6 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
}
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
// localId callers attach it via a checksum-equality join, so the local's
// bytes are the remote's — key local renders by the same checksum.
RemoteAsset toDto({String? localId}) => RemoteAsset(
id: id,
name: name,
@@ -74,7 +72,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
visibility: visibility,
livePhotoVideoId: livePhotoVideoId,
localId: localId,
localChecksum: localId == null ? null : checksum,
stackId: stackId,
isEdited: isEdited,
deletedAt: deletedAt,
@@ -34,53 +34,23 @@ class DriftBackupRepository extends DriftDatabaseRepository {
/// - total: number of distinct assets in selected albums, excluding those that are also in excluded albums
/// - backup: number of those assets that already exist on the server for [userId]
/// - remainder: number of those assets that do not yet exist on the server for [userId]
/// (includes processing), excluding handled iOS reverts (syncedChecksum == checksum
/// with the prior upload still on the server trashed counts, like the
/// checksum arm; only a hard delete re-opens the asset)
/// (includes processing)
/// - processing: number of those assets that are still preparing/have a null checksum
Future<({int total, int remainder, int processing})> getAllCounts(String userId) async {
const sql = '''
SELECT
COUNT(*) AS total_count,
COUNT(*) FILTER (WHERE lae.checksum IS NULL) AS processing_count,
COUNT(*) FILTER (
WHERE rae.id IS NULL
AND (
lae.checksum IS NULL
OR lae.synced_checksum IS NULL
OR lae.synced_checksum != lae.checksum
OR NOT EXISTS (
SELECT 1 FROM main.remote_asset_entity pr
WHERE pr.id = lae.prior_remote_id
)
)
) AS remainder_count
COUNT(*) FILTER (WHERE rae.id IS NULL) AS remainder_count
FROM local_asset_entity lae
LEFT JOIN main.remote_asset_entity rae
ON lae.checksum = rae.checksum AND rae.owner_id = ?1
WHERE (
EXISTS (
WHERE EXISTS (
SELECT 1
FROM local_album_asset_entity laa
INNER JOIN main.local_album_entity la on laa.album_id = la.id
WHERE laa.asset_id = lae.id
AND la.backup_selection = ?2
)
-- iOS burst: a hidden member inherits candidacy from its representative,
-- which is the one actually in the user's selected album.
OR (lae.is_burst_representative = 0 AND lae.burst_id IS NOT NULL AND EXISTS (
SELECT 1 FROM local_asset_entity rep
INNER JOIN local_album_asset_entity laa ON laa.asset_id = rep.id
INNER JOIN main.local_album_entity la ON la.id = laa.album_id
WHERE rep.burst_id = lae.burst_id AND rep.is_burst_representative = 1
AND la.backup_selection = ?2
-- exclude-wins propagates to the burst: the rep must not be excluded
AND NOT EXISTS (
SELECT 1 FROM local_album_asset_entity laa2
INNER JOIN main.local_album_entity la2 ON la2.id = laa2.album_id
WHERE laa2.asset_id = rep.id AND la2.backup_selection = ?3
)
))
)
AND NOT EXISTS (
SELECT 1
@@ -104,7 +74,6 @@ class DriftBackupRepository extends DriftDatabaseRepository {
.getSingle();
final data = row.data;
return (
total: (data['total_count'] as int?) ?? 0,
remainder: (data['remainder_count'] as int?) ?? 0,
@@ -112,60 +81,22 @@ class DriftBackupRepository extends DriftDatabaseRepository {
);
}
/// Backup candidates. With [burstId], scoped to the non-representative members
/// of that burst used to re-enqueue a burst's gated frames once its
/// representative has uploaded, without re-walking (and re-enqueuing) assets
/// already in flight from the main pass.
Future<List<LocalAsset>> getCandidates(String userId, {bool onlyHashed = true, String? burstId}) async {
Future<List<LocalAsset>> getCandidates(String userId, {bool onlyHashed = true}) async {
final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true)
..addColumns([_db.localAlbumEntity.id])
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected));
// iOS burst: a hidden member isn't a member of the user album its rep sits in
// (Photos only adds the cover), so it inherits backup candidacy from its rep.
// Matched with a correlated EXISTS (var-safe, mirrors getAllCounts) instead of
// materialising the burst-id list a large library could blow the SQLite
// variable limit.
final rep = _db.localAssetEntity.createAlias('rep');
final query = _db.localAssetEntity.select()
..where(
(lae) =>
(existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(
_db.localAlbumAssetEntity.albumId.isInQuery(selectedAlbumIds) &
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
),
) |
(lae.isBurstRepresentative.equals(false) &
lae.burstId.isNotNull() &
existsQuery(
rep.selectOnly()
..addColumns([rep.id])
..join([
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(rep.id),
useColumns: false,
),
innerJoin(
_db.localAlbumEntity,
_db.localAlbumEntity.id.equalsExp(_db.localAlbumAssetEntity.albumId),
useColumns: false,
),
])
..where(
rep.burstId.equalsExp(lae.burstId) &
rep.isBurstRepresentative.equals(true) &
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
// exclude-wins propagates to the burst: a member only
// inherits candidacy if its rep is itself a candidate
// (in a selected album AND not in an excluded one).
rep.id.isNotInQuery(_getExcludedSubquery()),
),
))) &
existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(
_db.localAlbumAssetEntity.albumId.isInQuery(selectedAlbumIds) &
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
),
) &
notExistsQuery(
_db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.checksum])
@@ -173,20 +104,6 @@ class DriftBackupRepository extends DriftDatabaseRepository {
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & _db.remoteAssetEntity.ownerId.equals(userId),
),
) &
// iOS revert: a reverted local hashes fresh (matches nothing remote),
// but if it was already reconciled (syncedChecksum == current checksum)
// it's handled, so don't re-queue it as a fresh upload. Suppress while
// the prior row exists at all trashed stays suppressed (same
// convention as the checksum arm above); only a hard-deleted remote
// must become a candidate again.
(lae.checksum.isNull() |
lae.syncedChecksum.isNull() |
lae.syncedChecksum.equalsExp(lae.checksum).not() |
notExistsQuery(
_db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id])
..where(_db.remoteAssetEntity.id.equalsExp(lae.priorRemoteId)),
)) &
lae.id.isNotInQuery(_getExcludedSubquery()),
)
..orderBy([(localAsset) => OrderingTerm.desc(localAsset.createdAt)]);
@@ -195,10 +112,6 @@ class DriftBackupRepository extends DriftDatabaseRepository {
query.where((lae) => lae.checksum.isNotNull());
}
if (burstId != null) {
query.where((lae) => lae.burstId.equals(burstId) & lae.isBurstRepresentative.equals(false));
}
return query.map((localAsset) => localAsset.toDto()).get();
}
}
@@ -120,7 +120,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 31;
int get schemaVersion => 30;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -311,14 +311,6 @@ class Drift extends $Drift {
from29To30: (m, v30) async {
await m.alterTable(TableMigration(v30.settings));
},
from30To31: (m, v31) async {
await m.addColumn(v31.localAssetEntity, v31.localAssetEntity.priorRemoteId);
await m.addColumn(v31.localAssetEntity, v31.localAssetEntity.syncedChecksum);
await m.addColumn(v31.localAssetEntity, v31.localAssetEntity.burstId);
await m.addColumn(v31.localAssetEntity, v31.localAssetEntity.isBurstRepresentative);
await m.createIndex(v31.idxLocalAssetPriorRemoteId);
await m.createIndex(v31.idxLocalAssetBurstId);
},
),
);
@@ -118,8 +118,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
i4.idxLocalAssetChecksum,
i4.idxLocalAssetCloudId,
i4.idxLocalAssetCreatedAt,
i4.idxLocalAssetPriorRemoteId,
i4.idxLocalAssetBurstId,
i3.idxStackPrimaryAssetId,
i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum,
@@ -15920,679 +15920,6 @@ i1.GeneratedColumn<String> _column_224(String aliasedName) =>
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
final class Schema31 extends i0.VersionedSchema {
Schema31({required super.database}) : super(version: 31);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxLocalAssetCreatedAt,
idxLocalAssetPriorRemoteId,
idxLocalAssetBurstId,
idxStackPrimaryAssetId,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
settings,
assetOcrEntity,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
idxAssetOcrAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 remoteAssetEntity = Shape50(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_212,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape52 localAssetEntity = Shape52(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
_column_225,
_column_226,
_column_227,
_column_228,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
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 idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxLocalAssetCreatedAt = i1.Index(
'idx_local_asset_created_at',
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
);
final i1.Index idxLocalAssetPriorRemoteId = i1.Index(
'idx_local_asset_prior_remote_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_prior_remote_id ON local_asset_entity (prior_remote_id)',
);
final i1.Index idxLocalAssetBurstId = i1.Index(
'idx_local_asset_burst_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_burst_id ON local_asset_entity (burst_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
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)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
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_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
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_159, _column_177],
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_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
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_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
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_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 trashedLocalAssetEntity = Shape47(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_205,
_column_131,
_column_120,
_column_132,
_column_206,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 settings = Shape49(
source: i0.VersionedTable(
entityName: 'settings',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_224, _column_115],
attachedDatabase: database,
),
alias: null,
);
late final Shape51 assetOcrEntity = Shape51(
source: i0.VersionedTable(
entityName: 'asset_ocr_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_213,
_column_214,
_column_215,
_column_216,
_column_217,
_column_218,
_column_219,
_column_220,
_column_221,
_column_222,
_column_223,
_column_201,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
final i1.Index idxAssetOcrAssetId = i1.Index(
'idx_asset_ocr_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
);
}
class Shape52 extends i0.VersionedTable {
Shape52({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationMs =>
columnsByName['duration_ms']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get iCloudId =>
columnsByName['i_cloud_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get adjustmentTime =>
columnsByName['adjustment_time']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<double> get latitude =>
columnsByName['latitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<double> get longitude =>
columnsByName['longitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<int> get playbackStyle =>
columnsByName['playback_style']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get priorRemoteId =>
columnsByName['prior_remote_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get syncedChecksum =>
columnsByName['synced_checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get burstId =>
columnsByName['burst_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isBurstRepresentative =>
columnsByName['is_burst_representative']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_225(String aliasedName) =>
i1.GeneratedColumn<String>(
'prior_remote_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<String> _column_226(String aliasedName) =>
i1.GeneratedColumn<String>(
'synced_checksum',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<String> _column_227(String aliasedName) =>
i1.GeneratedColumn<String>(
'burst_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<int> _column_228(String aliasedName) =>
i1.GeneratedColumn<int>(
'is_burst_representative',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints:
'NOT NULL DEFAULT 0 CHECK (is_burst_representative IN (0, 1))',
defaultValue: const i1.CustomExpression('0'),
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -16623,7 +15950,6 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -16772,11 +16098,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from29To30(migrator, schema);
return 30;
case 30:
final schema = Schema31(database: database);
final migrator = i1.Migrator(database, schema);
await from30To31(migrator, schema);
return 31;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -16813,7 +16134,6 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -16845,6 +16165,5 @@ i1.OnUpgrade stepByStep({
from27To28: from27To28,
from28To29: from28To29,
from29To30: from29To30,
from30To31: from30To31,
),
);
@@ -235,47 +235,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
});
}
Future<List<LocalAsset>> getAssetsToHash(String albumId) async {
// iOS burst: hidden members live in the smart album, not a user album, so a
// burst added to a backup album would never hash its other frames. Let a
// member inherit hashing from its representative via a correlated EXISTS
// (var-safe a large library could blow the SQLite variable limit otherwise).
final rep = _db.localAssetEntity.createAlias('rep');
Future<List<LocalAsset>> getAssetsToHash(String albumId) {
final query =
_db.localAlbumAssetEntity.select().join([
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAssetEntity.checksum.isNull())
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]);
final query = _db.localAssetEntity.select()
..where(
(lae) =>
lae.checksum.isNull() &
(existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(
_db.localAlbumAssetEntity.albumId.equals(albumId) &
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
),
) |
(lae.isBurstRepresentative.equals(false) &
lae.burstId.isNotNull() &
existsQuery(
rep.selectOnly()
..addColumns([rep.id])
..join([
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(rep.id),
useColumns: false,
),
])
..where(
rep.burstId.equalsExp(lae.burstId) &
rep.isBurstRepresentative.equals(true) &
_db.localAlbumAssetEntity.albumId.equals(albumId),
),
))),
)
..orderBy([(lae) => OrderingTerm.desc(lae.createdAt)]);
return query.map((row) => row.toDto()).get();
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
}
Future<void> updateCloudMapping(Map<String, String> cloudMapping) {
@@ -337,10 +305,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
latitude: Value(asset.latitude),
longitude: Value(asset.longitude),
adjustmentTime: Value(asset.adjustmentTime),
// Re-synced on every delta (DoUpdate carries the same companion) so a
// Photos re-pick that moves the representative flag is reflected.
burstId: Value(asset.burstId),
isBurstRepresentative: Value(asset.isBurstRepresentative),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity,
@@ -64,84 +64,6 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
});
}
Future<void> markSynced(String localId, {required String priorRemoteId, required String? syncedChecksum}) {
return (_db.localAssetEntity.update()..where((e) => e.id.equals(localId))).write(
LocalAssetEntityCompanion(priorRemoteId: Value(priorRemoteId), syncedChecksum: Value(syncedChecksum)),
);
}
/// The remote id to stack a burst member under: the prior_remote_id of any
/// already-uploaded member of the same burst (null until the first one lands).
/// The representative uploads first (non-reps gate on this), so the first hit
/// is the rep and `keepPrimary` pins it as the stack primary. Deliberately NOT
/// filtered to the representative: iOS can move the rep flag (a Photos re-pick),
/// and any in-stack member resolves to the same stack via linkAsset so
/// returning whichever member uploaded keeps every later frame in one stack
/// instead of spawning a second when the cover moves. Stable order so repeated
/// lookups pick the same anchor.
Future<String?> getBurstParentRemoteId(String burstId, {String? ownerId}) async {
// Prefer the remote id stamped by a frame this device already uploaded.
final row =
await (_db.localAssetEntity.selectOnly()
..addColumns([_db.localAssetEntity.priorRemoteId])
..where(_db.localAssetEntity.burstId.equals(burstId) & _db.localAssetEntity.priorRemoteId.isNotNull())
..orderBy([OrderingTerm.asc(_db.localAssetEntity.createdAt), OrderingTerm.asc(_db.localAssetEntity.id)])
..limit(1))
.getSingleOrNull();
final priorRemoteId = row?.read(_db.localAssetEntity.priorRemoteId);
if (priorRemoteId != null) {
return priorRemoteId;
}
if (ownerId == null) {
return null;
}
// Pre-existing burst: the representative was backed up before this feature, so
// no local frame ever stamped a prior. Anchor onto the rep's already-synced
// remote row (matched by checksum) so the hidden members can still stack.
final rep =
await (_db.localAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id])
..join([
innerJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum) &
_db.remoteAssetEntity.ownerId.equals(ownerId),
useColumns: false,
),
])
..where(
_db.localAssetEntity.burstId.equals(burstId) & _db.localAssetEntity.isBurstRepresentative.equals(true),
)
..orderBy([OrderingTerm.asc(_db.localAssetEntity.createdAt), OrderingTerm.asc(_db.localAssetEntity.id)])
..limit(1))
.getSingleOrNull();
return rep?.read(_db.remoteAssetEntity.id);
}
/// Whether the burst group still has a representative frame. A group can end up
/// rep-less (every frame is_burst_representative=0) after a Photos "Keep
/// Everything" / re-pick — its members can never anchor a stack, so callers
/// upload them standalone instead of gating forever.
Future<bool> burstHasRepresentative(String burstId) async {
final row =
await (_db.localAssetEntity.selectOnly()
..addColumns([_db.localAssetEntity.id])
..where(
_db.localAssetEntity.burstId.equals(burstId) & _db.localAssetEntity.isBurstRepresentative.equals(true),
)
..limit(1))
.getSingleOrNull();
return row != null;
}
/// Drops the edit-stacking stamps so the next backup cycle re-resolves the
/// asset from scratch (used when the server says the stamped prior is gone).
Future<void> clearSyncStamps(String localId) {
return (_db.localAssetEntity.update()..where((e) => e.id.equals(localId))).write(
const LocalAssetEntityCompanion(priorRemoteId: Value(null), syncedChecksum: Value(null)),
);
}
Future<void> delete(List<String> ids) {
if (ids.isEmpty) {
return Future.value();
@@ -46,9 +46,7 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
return query.map((row) {
final asset = row.readTable(_db.remoteAssetEntity).toDto();
final localId = row.read(_db.localAssetEntity.id);
// checksum-equality join: the local's bytes are the remote's
return asset.copyWith(localId: localId, localChecksum: localId == null ? null : asset.checksum);
return asset.copyWith(localId: row.read(_db.localAssetEntity.id));
});
}
@@ -1,156 +1,23 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/stack.model.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class StackReconcileTarget {
final String stackId;
final String newPrimaryId;
final String localAssetId;
final String localAssetChecksum;
const StackReconcileTarget({
required this.stackId,
required this.newPrimaryId,
required this.localAssetId,
required this.localAssetChecksum,
});
}
enum PriorState { live, trashed, missing }
class DriftStackRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftStackRepository(this._db) : super(_db);
// Find stacks whose primary should flip back after a revert: a local that was
// uploaded as an edit (prior in the stack) now hashes to a DIFFERENT member
// that isn't the primary. Two discriminators keep this from fighting stacks
// the user arranged by hand: the matched member must not be the local's own
// prior (a true revert has prior = the edit, member = the base), and the
// local must be unreconciled (synced_checksum != checksum the flip below
// writes synced = checksum, which is what makes this self-limiting). Driven
// from stack_entity so the work scales with the number of stacks (few), and
// runs every hash cycle so a flip that failed offline gets retried.
Future<List<StackReconcileTarget>> findRevertReconcileTargets() async {
final rows = await _db
.customSelect(
'''
SELECT
s.id AS stack_id,
member.id AS new_primary,
local.id AS local_id,
local.checksum AS local_checksum
FROM stack_entity s
INNER JOIN remote_asset_entity member
ON member.stack_id = s.id
AND member.deleted_at IS NULL
INNER JOIN local_asset_entity local
ON local.checksum = member.checksum
AND local.prior_remote_id IS NOT NULL
AND local.prior_remote_id != member.id
AND local.synced_checksum IS NOT local.checksum
INNER JOIN remote_asset_entity prior
ON prior.id = local.prior_remote_id
AND prior.stack_id = s.id
AND prior.deleted_at IS NULL
WHERE s.primary_asset_id != member.id
''',
readsFrom: {_db.localAssetEntity, _db.remoteAssetEntity, _db.stackEntity},
)
.get();
Future<List<Stack>> getAll(String userId) {
final query = _db.stackEntity.select()..where((e) => e.ownerId.equals(userId));
return rows
.map(
(row) => StackReconcileTarget(
stackId: row.read<String>('stack_id'),
newPrimaryId: row.read<String>('new_primary'),
localAssetId: row.read<String>('local_id'),
localAssetChecksum: row.read<String>('local_checksum'),
),
)
.toList();
}
// A trashed or locked-folder (visibility = 3) remote can't be stacked onto,
// so it reads as trashed; anything else is live.
PriorState _stateFromBlocked(bool blocked) => blocked ? PriorState.trashed : PriorState.live;
// What the synced remote table knows about a stamped prior. missing is
// ambiguous: either just uploaded and not synced back yet, or hard-deleted on
// the server the caller tells them apart via syncedChecksum (null = a chain
// is still mid-flight, so the row simply hasn't synced yet). A locked-folder
// row counts as trashed: the server refuses to stack onto it (and with a
// message the dead-parent belt doesn't match), so defer until it's unlocked.
Future<PriorState> priorState(String remoteId) async {
final row = await _db
.customSelect(
// 3 = locked
'SELECT (deleted_at IS NOT NULL OR visibility = 3) AS blocked FROM remote_asset_entity WHERE id = ? LIMIT 1',
variables: [Variable<String>(remoteId)],
readsFrom: {_db.remoteAssetEntity},
)
.getSingleOrNull();
if (row == null) {
return PriorState.missing;
}
return _stateFromBlocked(row.read<bool>('blocked'));
}
// The synced remote owned by [ownerId] with these exact bytes, if any. The
// server keys assets by (owner, checksum), so at most one row matches.
// Locked rows count as trashed here too, same reasoning as [priorState].
Future<({PriorState state, String? remoteId})> remoteByChecksum(String checksum, String ownerId) async {
final row = await _db
.customSelect(
// 3 = locked
'SELECT id, (deleted_at IS NOT NULL OR visibility = 3) AS blocked FROM remote_asset_entity WHERE checksum = ? AND owner_id = ? LIMIT 1',
variables: [Variable<String>(checksum), Variable<String>(ownerId)],
readsFrom: {_db.remoteAssetEntity},
)
.getSingleOrNull();
if (row == null) {
return (state: PriorState.missing, remoteId: null);
}
return (state: _stateFromBlocked(row.read<bool>('blocked')), remoteId: row.read<String>('id'));
}
// The stack a remote asset belongs to, if any. Used by the revert path to find
// the stack from prior_remote_id when the reverted bytes can't be checksum-matched.
Future<String?> findStackIdByRemoteId(String remoteId) async {
final row = await _db
.customSelect(
'SELECT stack_id FROM remote_asset_entity WHERE id = ? AND stack_id IS NOT NULL AND deleted_at IS NULL',
variables: [Variable<String>(remoteId)],
readsFrom: {_db.remoteAssetEntity},
)
.getSingleOrNull();
return row?.read<String?>('stack_id');
}
// The stack's original base member to flip back to on revert: the earliest-
// uploaded member that isn't the (latest-edit) prior. The base is uploaded
// before its edits, so oldest uploaded_at = the original.
Future<String?> findStackBaseId(String stackId, {required String excludeId}) async {
final row = await _db
.customSelect(
'''
SELECT id FROM remote_asset_entity
WHERE stack_id = ? AND id != ? AND deleted_at IS NULL
ORDER BY uploaded_at IS NULL, uploaded_at ASC, id ASC
LIMIT 1
''',
variables: [Variable<String>(stackId), Variable<String>(excludeId)],
readsFrom: {_db.remoteAssetEntity},
)
.getSingleOrNull();
return row?.read<String?>('id');
}
// Optimistic local primary flip so the timeline updates immediately; the
// server's stack-update websocket rewrites it shortly after.
Future<void> setPrimary(String stackId, String primaryAssetId) {
return (_db.stackEntity.update()..where((e) => e.id.equals(stackId))).write(
StackEntityCompanion(primaryAssetId: Value(primaryAssetId)),
);
return query.map((stack) {
return stack.toDto();
}).get();
}
}
extension on StackEntityData {
Stack toDto() {
return Stack(id: id, createdAt: createdAt, updatedAt: updatedAt, ownerId: ownerId, primaryAssetId: primaryAssetId);
}
}
@@ -3,7 +3,6 @@ import 'dart:io';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
class StorageRepository {
@@ -151,34 +150,4 @@ class StorageRepository {
log.warning("Error deleting temporary directory", error, stackTrace);
}
}
/// Base originals for the edit chain live under Library/Caches (immich_base),
/// not tmp, so [clearCache] can't wipe a chain still in flight across
/// launches. Sweeps only files older than a day: live chains and concurrent
/// foreground pair uploads keep their temps; orphans from dead chains go.
Future<void> clearEditBaseCache() async {
if (!CurrentPlatform.isIOS) {
return;
}
try {
final cache = await getApplicationCacheDirectory();
final dir = Directory('${cache.path}/immich_base');
if (!await dir.exists()) {
return;
}
final cutoff = DateTime.now().subtract(const Duration(days: 1));
await for (final entry in dir.list()) {
try {
final stat = await entry.stat();
if (stat.modified.isBefore(cutoff)) {
await entry.delete(recursive: true);
}
} catch (error, stackTrace) {
log.warning("Error sweeping ${entry.path}", error, stackTrace);
}
}
} catch (error, stackTrace) {
log.warning("Error sweeping edit base cache", error, stackTrace);
}
}
}
@@ -16,7 +16,6 @@ import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dar
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/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
@@ -72,13 +71,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
await _db.assetOcrEntity.deleteAll();
// The edit-stacking stamps point at remote rows wiped above; left in
// place they'd make the next backup (possibly a different account or
// server) stack onto ids that no longer exist. Only stamped rows need
// clearing, so skip the full-table rewrite when none are set.
await (_db.localAssetEntity.update()
..where((e) => e.priorRemoteId.isNotNull() | e.syncedChecksum.isNotNull()))
.write(const LocalAssetEntityCompanion(priorRemoteId: Value(null), syncedChecksum: Value(null)));
});
} finally {
// re-enable FK even if the transaction throws, otherwise the connection
@@ -203,27 +195,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
// websocket events are a point-in-time snapshot, so on the fast path don't overwrite
// link state the checkpoint sync owns (a motion video uploads visible then gets hidden).
RemoteAssetEntityCompanion _conflictUpdate(RemoteAssetEntityCompanion companion, bool fromWebsocket) {
if (!fromWebsocket) {
return companion;
}
// deletedAt is checkpoint-owned too: a debounced upload-ready snapshot always
// carries null and must not resurrect an asset trashed in the meantime.
return companion.copyWith(
visibility: const Value.absent(),
livePhotoVideoId: const Value.absent(),
stackId: const Value.absent(),
deletedAt: const Value.absent(),
);
}
Future<void> updateAssetsV1(
Iterable<SyncAssetV1> data, {
String debugLabel = 'user',
bool fromWebsocket = false,
}) async {
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data, {String debugLabel = 'user'}) async {
try {
await _db.batch((batch) {
for (final asset in data) {
@@ -252,7 +224,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
onConflict: DoUpdate((_) => _conflictUpdate(companion, fromWebsocket)),
onConflict: DoUpdate((_) => companion),
);
}
});
@@ -262,11 +234,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
Future<void> updateAssetsV2(
Iterable<SyncAssetV2> data, {
String debugLabel = 'user',
bool fromWebsocket = false,
}) async {
Future<void> updateAssetsV2(Iterable<SyncAssetV2> data, {String debugLabel = 'user'}) async {
try {
await _db.batch((batch) {
for (final asset in data) {
@@ -295,7 +263,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
onConflict: DoUpdate((_) => _conflictUpdate(companion, fromWebsocket)),
onConflict: DoUpdate((_) => companion),
);
}
});
@@ -928,6 +896,71 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
/// Replaces all OCR rows for [assetId] with [data] (e.g. after an asset edit re-runs OCR).
Future<void> replaceAssetOcr(String assetId, Iterable<AssetOcrResponseDto> data) async {
try {
await _db.batch((batch) {
batch.deleteWhere(_db.assetOcrEntity, (row) => row.assetId.equals(assetId));
for (final ocr in data) {
batch.insert(
_db.assetOcrEntity,
AssetOcrEntityCompanion(
id: Value(ocr.id),
assetId: Value(ocr.assetId),
recognizedText: Value(ocr.text),
x1: Value(ocr.x1),
y1: Value(ocr.y1),
x2: Value(ocr.x2),
y2: Value(ocr.y2),
x3: Value(ocr.x3),
y3: Value(ocr.y3),
x4: Value(ocr.x4),
y4: Value(ocr.y4),
boxScore: Value(ocr.boxScore),
textScore: Value(ocr.textScore),
isVisible: const Value(true),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: replaceAssetOcr', error, stack);
rethrow;
}
}
Future<void> replaceAssetFaces(String assetId, Iterable<AssetFaceResponseDto> data) async {
try {
await _db.batch((batch) {
batch.deleteWhere(_db.assetFaceEntity, (row) => row.assetId.equals(assetId));
for (final face in data) {
batch.insert(
_db.assetFaceEntity,
AssetFaceEntityCompanion(
id: Value(face.id),
assetId: Value(assetId),
personId: Value(face.person?.id),
imageWidth: Value(face.imageWidth),
imageHeight: Value(face.imageHeight),
boundingBoxX1: Value(face.boundingBoxX1),
boundingBoxY1: Value(face.boundingBoxY1),
boundingBoxX2: Value(face.boundingBoxX2),
boundingBoxY2: Value(face.boundingBoxY2),
sourceType: Value(face.sourceType.orElse(null)?.value ?? SourceType.machineLearning.value),
isVisible: const Value(true),
deletedAt: const Value(null),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: replaceAssetFaces', error, stack);
rethrow;
}
}
Future<void> pruneAssets() async {
try {
await _db.transaction(() async {
@@ -88,7 +88,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
livePhotoVideoId: row.livePhotoVideoId,
stackId: row.stackId,
isEdited: row.isEdited,
localChecksum: row.localChecksum,
)
: LocalAsset(
id: row.localId!,
+1
View File
@@ -263,6 +263,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
child: MaterialApp.router(
title: 'Immich',
debugShowCheckedModeBanner: true,
scaffoldMessengerKey: scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,

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