mirror of
https://github.com/immich-app/immich.git
synced 2026-02-03 18:48:01 -08:00
Compare commits
36 Commits
feat/dev_c
...
chore/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac7b35359 | ||
|
|
5a7042364b | ||
|
|
db0ea0f3a8 | ||
|
|
88c0243a20 | ||
|
|
3a29522df6 | ||
|
|
50eae23f3a | ||
|
|
95419750bb | ||
|
|
5fb858a865 | ||
|
|
18084a49ec | ||
|
|
f107cb044a | ||
|
|
f4e7ea47a6 | ||
|
|
8747fc4935 | ||
|
|
287fa79d75 | ||
|
|
bcfb5bee1f | ||
|
|
538263dc38 | ||
|
|
51aec1e93d | ||
|
|
53825cc3d6 | ||
|
|
6e7c2817a3 | ||
|
|
7bd79b551c | ||
|
|
5fe954b3c9 | ||
|
|
7f81a5bd6f | ||
|
|
37a79292c0 | ||
|
|
bf6211776f | ||
|
|
6c178a04dc | ||
|
|
036d314cb6 | ||
|
|
1fc5da398a | ||
|
|
4d84338086 | ||
|
|
0ac49b00ee | ||
|
|
e427778a96 | ||
|
|
b82e29fbb4 | ||
|
|
ff19aea4ac | ||
|
|
28179a3a1d | ||
|
|
af1e18d07e | ||
|
|
270a0ff986 | ||
|
|
9d3f10372d | ||
|
|
2f1385a236 |
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
||||
22.18.0
|
||||
22.19.0
|
||||
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -34,3 +34,7 @@ The `/api/something` endpoint is now `/api/something-else`
|
||||
- [ ] I have followed naming conventions/patterns in the surrounding code
|
||||
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
||||
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
||||
|
||||
## Please describe to which degree, if any, an LLM was used in creating this pull request.
|
||||
|
||||
...
|
||||
|
||||
14
Makefile
14
Makefile
@@ -10,14 +10,14 @@ dev-update: prepare-volumes
|
||||
dev-scale: prepare-volumes
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||
|
||||
dev-docs: prepare-volumes
|
||||
dev-docs:
|
||||
npm --prefix docs run start
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: prepare-volumes
|
||||
e2e:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
||||
|
||||
e2e-update: prepare-volumes
|
||||
e2e-update:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
e2e-down:
|
||||
@@ -73,6 +73,8 @@ define safe_chown
|
||||
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
|
||||
true; \
|
||||
else \
|
||||
STATUS=$$?; echo "Exit code: $$STATUS $(1)"; \
|
||||
echo "$$STATUS $(1)"; \
|
||||
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
|
||||
exit 1; \
|
||||
fi;
|
||||
@@ -83,11 +85,13 @@ prepare-volumes:
|
||||
@$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
|
||||
ifneq ($(UPLOAD_LOCATION),)
|
||||
ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
|
||||
@mkdir -p "docker/$(UPLOAD_LOCATION)"
|
||||
@mkdir -p "docker/$(UPLOAD_LOCATION)/photos/upload"
|
||||
@$(call safe_chown,docker/$(UPLOAD_LOCATION),)
|
||||
@$(call safe_chown,docker/$(UPLOAD_LOCATION)/photos,-R)
|
||||
else
|
||||
@mkdir -p "$(UPLOAD_LOCATION)"
|
||||
@mkdir -p "$(UPLOAD_LOCATION)/photos/upload"
|
||||
@$(call safe_chown,$(UPLOAD_LOCATION),)
|
||||
@$(call safe_chown,$(UPLOAD_LOCATION)/photos,-R)
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.18.0
|
||||
22.19.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.86",
|
||||
"version": "2.2.88",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.17.1",
|
||||
"@types/node": "^22.18.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -69,6 +69,6 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.18.0"
|
||||
"node": "22.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.18.0
|
||||
22.19.0
|
||||
|
||||
@@ -60,6 +60,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.18.0"
|
||||
"node": "22.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@@ -1,4 +1,12 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.141.1",
|
||||
"url": "https://v1.141.1.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.141.0",
|
||||
"url": "https://v1.141.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.140.1",
|
||||
"url": "https://v1.140.1.archive.immich.app"
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.18.0
|
||||
22.19.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.140.1",
|
||||
"version": "1.141.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.17.1",
|
||||
"@types/node": "^22.18.0",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -45,7 +45,7 @@
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.34.0",
|
||||
"sharp": "^0.34.3",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -54,6 +54,6 @@
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.18.0"
|
||||
"node": "22.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ describe('/partners', () => {
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
|
||||
createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
createPartner({ partnerCreateDto: { sharedWithId: user2.userId } }, { headers: asBearerAuth(user1.accessToken) }),
|
||||
createPartner({ partnerCreateDto: { sharedWithId: user1.userId } }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -462,7 +462,8 @@ export const utils = {
|
||||
updateLibrary: (accessToken: string, id: string, dto: UpdateLibraryDto) =>
|
||||
updateLibrary({ id, updateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
|
||||
createPartner: (accessToken: string, id: string) =>
|
||||
createPartner({ partnerCreateDto: { sharedWithId: id } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
updateMyPreferences: (accessToken: string, userPreferencesUpdateDto: UserPreferencesUpdateDto) =>
|
||||
updateMyPreferences({ userPreferencesUpdateDto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
@@ -396,6 +396,7 @@
|
||||
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
|
||||
"advanced_settings_proxy_headers_subtitle": "عرف عناوين الوكيل التي يستخدمها Immich لارسال كل طلب شبكي",
|
||||
"advanced_settings_proxy_headers_title": "عناوين الوكيل",
|
||||
"advanced_settings_readonly_mode_title": "وضع القراءة فقط",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "تخطي التحقق من شهادة SSL لخادم النقطة النهائي. مكلوب للشهادات الموقعة ذاتيا.",
|
||||
"advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "حذف او استعادة تلقائي للاصول على هذا الجهاز عند تنفيذ العملية على الويب",
|
||||
|
||||
29
i18n/az.json
29
i18n/az.json
@@ -1,37 +1,53 @@
|
||||
{
|
||||
"about": "Haqqinda",
|
||||
"about": "Haqqında",
|
||||
"account": "Hesab",
|
||||
"account_settings": "Hesab parametrləri",
|
||||
"acknowledge": "Təsdiq et",
|
||||
"action": "Əməliyyat",
|
||||
"action_common_update": "Yenilə",
|
||||
"actions": "Əməliyyatlar",
|
||||
"active": "Aktiv",
|
||||
"activity": "Fəaliyyət",
|
||||
"activity_changed": "Fəaliyyət {enabled, select, true {aktivdir} other {aktiv deyil}}",
|
||||
"add": "Əlavə et",
|
||||
"add_a_description": "Təsviri əlavə et",
|
||||
"add_a_location": "Məkan əlavə et",
|
||||
"add_a_name": "Ad əlavə et",
|
||||
"add_a_title": "Başlıq əlavə et",
|
||||
"add_birthday": "Doğum günü əlavə et",
|
||||
"add_endpoint": "Son nöqtə əlavə et",
|
||||
"add_exclusion_pattern": "İstisna nümunəsi əlavə et",
|
||||
"add_import_path": "Import yolunu əlavə et",
|
||||
"add_location": "Məkanı əlavə et",
|
||||
"add_location": "Məkan əlavə et",
|
||||
"add_more_users": "Daha çox istifadəçi əlavə et",
|
||||
"add_partner": "Partnyor əlavə et",
|
||||
"add_path": "Yol əlavə et",
|
||||
"add_photos": "Şəkilləri əlavə et",
|
||||
"add_to": "... əlavə et",
|
||||
"add_photos": "Şəkillər əlavə et",
|
||||
"add_tag": "Etiket əlavə et",
|
||||
"add_to": "Bura əlavə et…",
|
||||
"add_to_album": "Albom əlavə et",
|
||||
"add_to_album_bottom_sheet_added": "{album} albomuna əlavə edildi",
|
||||
"add_to_album_bottom_sheet_already_exists": "Artıq {album} albomunda var",
|
||||
"add_to_album_toggle": "{album} üçün seçimi dəyişin",
|
||||
"add_to_albums": "Albomlara əlavə et",
|
||||
"add_to_albums_count": "Albomlara əlavə et ({count})",
|
||||
"add_to_shared_album": "Paylaşılan alboma əlavə et",
|
||||
"add_url": "URL əlavə et",
|
||||
"added_to_archive": "Arxivə əlavə edildi",
|
||||
"added_to_favorites": "Sevimlilələrə əlavə edildi",
|
||||
"added_to_favorites_count": "{count, number} şəkil sevimlilələrə əlavə edildi",
|
||||
"admin": {
|
||||
"add_exclusion_pattern_description": "İstisna şablonlarını əlavə edin. *, ** və ? ilə Globbing dəstəklənir. Məs.: \"Raw\" adlanan hər hansısa bir qovluqda bütün faylları saymamaq üçün \"**/Raw/**\"-dan istifadə edin. \".tif\" ilə bitən bütün faylları saymamaq üçün \"**/*.tif\"-dən istifadə edin. Faylı mütləq yoldan istifadə etməklə saymamaq istəyirsinizsə \"/path/to/ignore/**\"-dan istifadə edin.",
|
||||
"admin_user": "Admin İstifadəçi",
|
||||
"asset_offline_description": "Bu xarici kitabxana varlığı diskdə artıq tapılmadı və zibil qutusuna köçürüldü. Əgər fayl kitabxana içərisində köçürülübsə, zaman şkalanızı yeni uyğun gələn varlıq üçün yoxlayın. Varlığı yenidən qaytarmaq üçün aşağıda verilmiş fayl yolunun Immich tərəfindən əlçatan olduğundan əmin olduqdan sonra kitabxananı skan edin.",
|
||||
"authentication_settings": "Səlahiyyətləndirmə parametrləri",
|
||||
"authentication_settings_description": "Şifrə, OAuth və digər səlahiyyətləndirmə parametrləri",
|
||||
"authentication_settings_disable_all": "Bütün giriş etmə metodlarını söndürmək istədiyinizdən əminsinizmi? Giriş etmə funksiyası tamamilə söndürüləcəkdir.",
|
||||
"authentication_settings_reenable": "Yenidən aktiv etmək üçün <link> Server Əmri</link> -ni istifadə edin.",
|
||||
"background_task_job": "Arxa plan tapşırıqları",
|
||||
"backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
|
||||
"backup_database": "Verilənlər bazasının dump-ını yaradın",
|
||||
"backup_database_enable_description": "Verilənlər bazasının artıq nüsxələrini aktiv et",
|
||||
"backup_keep_last_amount": "Tutulması gərəkən nüsxələrin sayı",
|
||||
"backup_settings": "Ehtiyat Nüsxə Parametrləri",
|
||||
"backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
|
||||
"config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
|
||||
@@ -84,5 +100,6 @@
|
||||
"machine_learning_facial_recognition": "Üz Tanıma",
|
||||
"machine_learning_facial_recognition_description": "Şəkillərdəki üzləri aşkarla, tanı və qruplaşdır",
|
||||
"machine_learning_facial_recognition_model": "Üz tanıma modeli"
|
||||
}
|
||||
},
|
||||
"timeline": "Zaman şkalası"
|
||||
}
|
||||
|
||||
@@ -399,6 +399,8 @@
|
||||
"purchase_button_buy": "Купіць",
|
||||
"purchase_button_buy_immich": "Купіць Immich",
|
||||
"purchase_button_select": "Выбраць",
|
||||
"readonly_mode_enabled": "Уключаны рэжым толькі для чытання",
|
||||
"reassign": "Перапрызначыць",
|
||||
"remove": "Выдаліць",
|
||||
"remove_from_album": "Выдаліць з альбома",
|
||||
"remove_from_favorites": "Выдаліць з абраных",
|
||||
|
||||
24
i18n/bg.json
24
i18n/bg.json
@@ -396,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "Предпочитай изображенията на сървъра",
|
||||
"advanced_settings_proxy_headers_subtitle": "Дефиниране на прокси хедъри, които Immich трябва да изпраща с всяка мрежова заявка",
|
||||
"advanced_settings_proxy_headers_title": "Прокси хедъри",
|
||||
"advanced_settings_readonly_mode_subtitle": "Активира режима \"само за четене\", при който снимките могат да бъдат разглеждани, но неща като избор на няколко изображения, споделяне, изтриване са забранени. Активиране/деактивиране на режима само за четене става от картинката-аватар на потребителя от основния екран",
|
||||
"advanced_settings_readonly_mode_title": "Режим само за четене",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Пропуска проверката на SSL-сертификата на сървъра. Изисква се при самоподписани сертификати.",
|
||||
"advanced_settings_self_signed_ssl_title": "Разреши самоподписани SSL сертификати",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Автоматично изтрии или възстанови обект на това устройство, когато действието е извършено през уеб-интерфейса",
|
||||
@@ -461,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "Излез от профила",
|
||||
"app_settings": "Настройки ма приложението",
|
||||
"appears_in": "Излиза в",
|
||||
"apply_count": "Приложи ({count, number})",
|
||||
"archive": "Архив",
|
||||
"archive_action_prompt": "{count} са добавени в Архива",
|
||||
"archive_or_unarchive_photo": "Архивиране или деархивиране на снимка",
|
||||
@@ -500,7 +503,7 @@
|
||||
"assets": "Елементи",
|
||||
"assets_added_count": "Добавено {count, plural, one {# asset} other {# assets}}",
|
||||
"assets_added_to_album_count": "Добавен(и) са {count, plural, one {# актив} other {# актива}} в албума",
|
||||
"assets_added_to_albums_count": "Добавени са {assetTotal} обекта в {albumTotal} албума",
|
||||
"assets_added_to_albums_count": "{assetTotal, plural, one {# обект е добавен} other {# обекта са добавени}} в {albumTotal, plural, one {# албум} other {# албума}}",
|
||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Обекта не може да се добави} other {Обектите не може да се добавят}} в албума",
|
||||
"assets_cannot_be_added_to_albums": "{count, plural, one {обект не може да бъде добавен} other {обекта не могат да бъдат добавени}} в никой от албумите",
|
||||
"assets_count": "{count, plural, one {# актив} other {# актива}}",
|
||||
@@ -912,7 +915,7 @@
|
||||
"error_selecting_all_assets": "Грешка при избора на всички файлове",
|
||||
"exclusion_pattern_already_exists": "Този модел за изключване вече съществува.",
|
||||
"failed_to_create_album": "Неуспешно създаване на албум",
|
||||
"failed_to_create_shared_link": "Неуспешно създаване на споделена връзка",
|
||||
"failed_to_create_shared_link": "Неуспешно създаване на спoделена връзка",
|
||||
"failed_to_edit_shared_link": "Неуспешно редактиране на споделена връзка",
|
||||
"failed_to_get_people": "Неуспешно зареждане на хора",
|
||||
"failed_to_keep_this_delete_others": "Неуспешно запазване на този обект и изтриване на останалите обекти",
|
||||
@@ -1073,12 +1076,18 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "За да работи тази функция зарежда външни ресурси от Google.",
|
||||
"general": "Общи",
|
||||
"geolocation_instruction_all_have_location": "Всички обекти от тази дата вече имат данни за местоположение. Опитайте да включите показване на всички обекти или изберете друга дата",
|
||||
"geolocation_instruction_location": "Изберете обект с GPS координати за да използвате тях или изберете място директно от картата",
|
||||
"geolocation_instruction_no_date": "Изберете дата за да управлявате данните за локация на снимките и видеата от тази дата",
|
||||
"geolocation_instruction_no_photos": "Не са намерени снимки или видеа от тази дата. Изберете друга дата",
|
||||
"get_help": "Помощ",
|
||||
"get_wifiname_error": "Неуспешно получаване името на Wi-Fi мрежата. Моля, убедете се, че са предоставени нужните разрешения на приложението и има връзка с Wi-Fi",
|
||||
"getting_started": "Как да започнем",
|
||||
"go_back": "Връщане назад",
|
||||
"go_to_folder": "Отиди в папката",
|
||||
"go_to_search": "Преминаване към търсене",
|
||||
"gps": "GPS координати",
|
||||
"gps_missing": "Няма GPS координати",
|
||||
"grant_permission": "Дай разрешение",
|
||||
"group_albums_by": "Групирай албум по...",
|
||||
"group_country": "Групирай по държава",
|
||||
@@ -1262,6 +1271,7 @@
|
||||
"main_branch_warning": "Използвате версия за разработчици, силно препоръчваме да използвате официална версия!",
|
||||
"main_menu": "Главно меню",
|
||||
"make": "Марка",
|
||||
"manage_geolocation": "Управление на местоположенията",
|
||||
"manage_shared_links": "Управление на споделени връзки",
|
||||
"manage_sharing_with_partners": "Управление на споделянето с партньори",
|
||||
"manage_the_app_settings": "Управление на настройките на приложението",
|
||||
@@ -1508,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "Мобилното приложение е остаряло. Моля, актуализирай до най-новата версия.",
|
||||
"profile_drawer_client_server_up_to_date": "Клиента и сървъра са обновени",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "Режима само за четене е активиран. С двоен клик върху картиката-аватар на потребителя ще деактивирате само за четене.",
|
||||
"profile_drawer_server_out_of_date_major": "Версията на сървъра е остаряла. Моля, актуализирай поне до последната главна версия.",
|
||||
"profile_drawer_server_out_of_date_minor": "Версията на сървъра е остаряла. Моля, актуализирай до последната версия.",
|
||||
"profile_image_of_user": "Профилна снимка на {user}",
|
||||
@@ -1553,6 +1564,8 @@
|
||||
"rating_description": "Покажи EXIF оценката в панела с информация",
|
||||
"reaction_options": "Избор на реакция",
|
||||
"read_changelog": "Прочети промените",
|
||||
"readonly_mode_disabled": "Режима само за четене е деактивиран",
|
||||
"readonly_mode_enabled": "Режима само за четене е активиран",
|
||||
"reassign": "Преназначаване",
|
||||
"reassigned_assets_to_existing_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на {name, select, null {съществуващ човек} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на нов човек",
|
||||
@@ -1722,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Създаването на албум не бе успешно",
|
||||
"selected": "Избрано",
|
||||
"selected_count": "{count, plural, other {# избрани}}",
|
||||
"selected_gps_coordinates": "избрани GPS координати",
|
||||
"send_message": "Изпратете съобщение",
|
||||
"send_welcome_email": "Изпратете имейл за добре дошли",
|
||||
"server_endpoint": "Адрес на сървъра",
|
||||
@@ -1832,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "Натиснете ⇧, за да изтриете завинаги елемента",
|
||||
"show_album_options": "Показване опции за албум",
|
||||
"show_albums": "Покажи албуми",
|
||||
"show_all_assets": "Покажи всичко",
|
||||
"show_all_people": "Покажи всички хора",
|
||||
"show_and_hide_people": "Показване и скриване на хора",
|
||||
"show_assets_without_location": "Покажи обекти без координати",
|
||||
"show_file_location": "Покажи местоположението на файла",
|
||||
"show_gallery": "Покажи галерия",
|
||||
"show_hidden_people": "Показване на скритите хора",
|
||||
@@ -1941,7 +1957,9 @@
|
||||
"to_change_password": "Промяна на паролата",
|
||||
"to_favorite": "Любим",
|
||||
"to_login": "Вписване",
|
||||
"to_multi_select": "за избор на няколко",
|
||||
"to_parent": "Отиди към родителския елемент",
|
||||
"to_select": "за избор",
|
||||
"to_trash": "Кошче",
|
||||
"toggle_settings": "Превключване на настройките",
|
||||
"total": "Общо",
|
||||
@@ -1991,6 +2009,7 @@
|
||||
"unstacked_assets_count": "Разкачени {count, plural, one {# елемент} other {# елементи}}",
|
||||
"untagged": "Немаркирани",
|
||||
"up_next": "Следващ",
|
||||
"update_location_action_prompt": "Обнови координатите на {count} избрани обекта с:",
|
||||
"updated_at": "Обновено",
|
||||
"updated_password": "Паролата е актуализирана",
|
||||
"upload": "Качване",
|
||||
@@ -2015,6 +2034,7 @@
|
||||
"use_biometric": "Използвай биометрия",
|
||||
"use_current_connection": "използвай текущата връзка",
|
||||
"use_custom_date_range": "Използвайте собствен диапазон от дати вместо това",
|
||||
"use_this_location": "Избери това място",
|
||||
"user": "Потребител",
|
||||
"user_has_been_deleted": "Този потребител е премахнат.",
|
||||
"user_id": "Потребител ИД",
|
||||
|
||||
@@ -14,5 +14,10 @@
|
||||
"add_exclusion_pattern": "Putem wan paten wae hemi karem aot",
|
||||
"add_import_path": "Putem wan pat blo import",
|
||||
"add_location": "Putem wan place blo hem",
|
||||
"add_more_users": "Putem mor man"
|
||||
"add_more_users": "Putem mor man",
|
||||
"readonly_mode_enabled": "Mod blo yu no save janjem i on",
|
||||
"reassigned_assets_to_new_person": "Janjem{count, plural, one {# asset} other {# assets}} blo nu man",
|
||||
"reassing_hint": "janjem ol sumtin yu bin joos i go blo wan man",
|
||||
"recent-albums": "album i no old tu mas",
|
||||
"recent_searches": "lukabout wea i no old tu mas"
|
||||
}
|
||||
|
||||
20
i18n/ca.json
20
i18n/ca.json
@@ -396,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "Prefereix imatges remotes",
|
||||
"advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa",
|
||||
"advanced_settings_proxy_headers_title": "Capçaleres de proxy",
|
||||
"advanced_settings_readonly_mode_subtitle": "Habilita el només de lectura mode on les fotos poden ser només vist, a coses els agrada seleccionant imatges múltiples, compartint, càsting, elimina és tot discapacitat. Habilita/Desactiva només de lectura via avatar d'usuari des de la pantalla major",
|
||||
"advanced_settings_readonly_mode_title": "Mode de només lectura",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Omet la verificació del certificat SSL del servidor. Requerit per a certificats autosignats.",
|
||||
"advanced_settings_self_signed_ssl_title": "Permet certificats SSL autosignats",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Suprimeix o restaura automàticament un actiu en aquest dispositiu quan es realitzi aquesta acció al web",
|
||||
@@ -461,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "Tanca la sessió",
|
||||
"app_settings": "Configuració de l'app",
|
||||
"appears_in": "Apareix a",
|
||||
"apply_count": "Aplicar ({count, number})",
|
||||
"archive": "Arxiu",
|
||||
"archive_action_prompt": "{count} afegit a Arxiu",
|
||||
"archive_or_unarchive_photo": "Arxivar o desarxivar fotografia",
|
||||
@@ -1073,12 +1076,18 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Aquesta funció carrega recursos externs de Google per funcionar.",
|
||||
"general": "General",
|
||||
"geolocation_instruction_all_have_location": "Tots els actius d'aquesta data ja tenen dades d'ubicació. Prova de mostrar tots els actius o selecciona una data diferent",
|
||||
"geolocation_instruction_location": "Fes click en un element amb coordinades GPS per utilitzar la seva ubicació o selecciona una ubicació des del mapa",
|
||||
"geolocation_instruction_no_date": "Seleccioneu una data per gestionar dades d'ubicació per a fotos i vídeos d'aquest dia",
|
||||
"geolocation_instruction_no_photos": "Cap foto o vídeo trobats per a aquesta data. Selecciona una data diferent",
|
||||
"get_help": "Aconseguir ajuda",
|
||||
"get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi",
|
||||
"getting_started": "Començant",
|
||||
"go_back": "Torna",
|
||||
"go_to_folder": "Anar al directori",
|
||||
"go_to_search": "Vés a cercar",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "Sense GPS",
|
||||
"grant_permission": "Concedir permís",
|
||||
"group_albums_by": "Agrupa àlbums per...",
|
||||
"group_country": "Agrupar per país",
|
||||
@@ -1262,6 +1271,7 @@
|
||||
"main_branch_warning": "Esteu utilitzant una versió en desenvolupament; Recomanem fer servir una versió publicada!",
|
||||
"main_menu": "Menú principal",
|
||||
"make": "Fabricant",
|
||||
"manage_geolocation": "Gestioneu la vostra ubicació",
|
||||
"manage_shared_links": "Administrar enllaços compartits",
|
||||
"manage_sharing_with_partners": "Gestiona la compartició amb els companys",
|
||||
"manage_the_app_settings": "Gestioneu la configuració de l'aplicació",
|
||||
@@ -1508,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "L'aplicació mòbil està desactualitzada. Si us plau, actualitzeu a l'última versió menor.",
|
||||
"profile_drawer_client_server_up_to_date": "El Client i el Servidor estan actualitzats",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "Manera de només lectura activada. Feu doble click a la icona de l'avatar de l'usuari per sortir.",
|
||||
"profile_drawer_server_out_of_date_major": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió major.",
|
||||
"profile_drawer_server_out_of_date_minor": "El servidor està desactualitzat. Si us plau, actualitzeu a l'última versió menor.",
|
||||
"profile_image_of_user": "Imatge de perfil de {user}",
|
||||
@@ -1553,6 +1564,8 @@
|
||||
"rating_description": "Mostrar la valoració EXIF al panell d'informació",
|
||||
"reaction_options": "Opcions de reacció",
|
||||
"read_changelog": "Llegeix el registre de canvis",
|
||||
"readonly_mode_disabled": "Mode de només lectura desactivat",
|
||||
"readonly_mode_enabled": "Mode de només lectura activat",
|
||||
"reassign": "Reassignar",
|
||||
"reassigned_assets_to_existing_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a {name, select, null {una persona existent} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova",
|
||||
@@ -1722,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Error al crear l'àlbum",
|
||||
"selected": "Seleccionat",
|
||||
"selected_count": "{count, plural, one {# seleccionat} other {# seleccionats}}",
|
||||
"selected_gps_coordinates": "seleccio de coordinades GPS",
|
||||
"send_message": "Envia missatge",
|
||||
"send_welcome_email": "Envia correu de benvinguda",
|
||||
"server_endpoint": "Endpoint de Servidor",
|
||||
@@ -1832,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment",
|
||||
"show_album_options": "Mostra les opcions d'àlbum",
|
||||
"show_albums": "Mostrar àlbums",
|
||||
"show_all_assets": "Mostrar tots els elements",
|
||||
"show_all_people": "Veure totes les persones",
|
||||
"show_and_hide_people": "Mostra i amaga persones",
|
||||
"show_assets_without_location": "Mostra els elements sense ubicació",
|
||||
"show_file_location": "Mostra l'ubicació del fitxer",
|
||||
"show_gallery": "Mostra la galeria",
|
||||
"show_hidden_people": "Mostra persones ocultes",
|
||||
@@ -1941,7 +1957,9 @@
|
||||
"to_change_password": "Canviar la contrasenya",
|
||||
"to_favorite": "Prefereix",
|
||||
"to_login": "Iniciar sessió",
|
||||
"to_multi_select": "per multi-seleccionar",
|
||||
"to_parent": "Anar als pares",
|
||||
"to_select": "per seleccionar",
|
||||
"to_trash": "Paperera",
|
||||
"toggle_settings": "Canvia configuració",
|
||||
"total": "Total",
|
||||
@@ -1991,6 +2009,7 @@
|
||||
"unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}",
|
||||
"untagged": "Sense etiqueta",
|
||||
"up_next": "Pròxim",
|
||||
"update_location_action_prompt": "Actualitza la ubicació de {count} elements seleccionats amb:",
|
||||
"updated_at": "Actualitzat",
|
||||
"updated_password": "Contrasenya actualitzada",
|
||||
"upload": "Pujar",
|
||||
@@ -2015,6 +2034,7 @@
|
||||
"use_biometric": "Empra biometria",
|
||||
"use_current_connection": "utilitzar la connexió actual",
|
||||
"use_custom_date_range": "Fes servir un rang de dates personalitzat",
|
||||
"use_this_location": "Fes clic per utilitzar la ubicació",
|
||||
"user": "Usuari",
|
||||
"user_has_been_deleted": "Aquest usuari ha sigut eliminat.",
|
||||
"user_id": "ID d'usuari",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Otevřít vyhledávací filtry",
|
||||
"options": "Možnosti",
|
||||
"or": "nebo",
|
||||
"organize_into_albums": "Organizovat do alb",
|
||||
"organize_into_albums_description": "Umístit existující fotky do alb s použitím aktuálního nastavení synchronizace",
|
||||
"organize_your_library": "Uspořádejte si knihovnu",
|
||||
"original": "originál",
|
||||
"other": "Ostatní",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Stav podporovatele",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Produktový klíč serveru spravuje správce",
|
||||
"query_asset_id": "ID položky dotazu",
|
||||
"queue_status": "Ve frontě {count}/{total}",
|
||||
"rating": "Hodnocení hvězdičkami",
|
||||
"rating_clear": "Vyčistit hodnocení",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album",
|
||||
"selected": "Vybráno",
|
||||
"selected_count": "{count, plural, one {# vybraný} few {# vybrané} other {# vybraných}}",
|
||||
"selected_gps_coordinates": "vybrané GPS souřadnice",
|
||||
"selected_gps_coordinates": "Vybrané GPS souřadnice",
|
||||
"send_message": "Odeslat zprávu",
|
||||
"send_welcome_email": "Poslat uvítací e-mail",
|
||||
"server_endpoint": "Koncový bod serveru",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Zobrazit další položku",
|
||||
"view_previous_asset": "Zobrazit předchozí položku",
|
||||
"view_qr_code": "Zobrazit QR kód",
|
||||
"view_similar_photos": "Zobrazit podobné fotky",
|
||||
"view_stack": "Zobrazit seskupení",
|
||||
"view_user": "Zobrazit uživatele",
|
||||
"viewer_remove_from_stack": "Odstranit ze zásobníku",
|
||||
|
||||
143
i18n/da.json
143
i18n/da.json
@@ -126,13 +126,13 @@
|
||||
"machine_learning_clip_model": "CLIP-model",
|
||||
"machine_learning_clip_model_description": "Navnet på CLIP-modellen på listen <link>her</link>. Bemærk at du skal genkøre \"Smart Søgning\"-jobbet for alle billeder, hvis du skifter model.",
|
||||
"machine_learning_duplicate_detection": "Dubletdetektion",
|
||||
"machine_learning_duplicate_detection_enabled": "Aktiver duplikatdetektion",
|
||||
"machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler blive de-duplikerede.",
|
||||
"machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige duplikater",
|
||||
"machine_learning_duplicate_detection_enabled": "Aktiver dubletdetektion",
|
||||
"machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler stadig blive de-duplikerede.",
|
||||
"machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige dubletter",
|
||||
"machine_learning_enabled": "Aktivér maskinlæring",
|
||||
"machine_learning_enabled_description": "Hvis deaktiveret, vil alle ML-funktioner blive deaktiveret uanset nedenstående indstillinger.",
|
||||
"machine_learning_facial_recognition": "Ansigtsgenkendelse",
|
||||
"machine_learning_facial_recognition_description": "Registrer, genkend og grupper ansigter i billeder",
|
||||
"machine_learning_facial_recognition_description": "Opdag, genkend og gruppér ansigter i billeder",
|
||||
"machine_learning_facial_recognition_model": "Ansigtsgenkendelsesmodel",
|
||||
"machine_learning_facial_recognition_model_description": "Modellerne er listet i faldende størrelsesorden. Større modeller er langsommere og bruger mere hukommelse, men giver bedre resultater. Bemærk, at du skal køre ansigtsopdagelsesopgaven igen for alle billeder, når du ændrer en model.",
|
||||
"machine_learning_facial_recognition_setting": "Aktivér ansigtgenkendelse",
|
||||
@@ -221,6 +221,8 @@
|
||||
"oauth_mobile_redirect_uri": "Mobilomdiregerings-URL",
|
||||
"oauth_mobile_redirect_uri_override": "Tilsidesættelse af mobil omdiregerings-URL",
|
||||
"oauth_mobile_redirect_uri_override_description": "Aktiver, når OAuth-udbyderen ikke tillader en mobil URI, som ''{callback}''",
|
||||
"oauth_role_claim": "Rolle attribut",
|
||||
"oauth_role_claim_description": "Tildel automatisk admin adgang på basis af forekomst af denne påstand. Dén kan være enten 'user' eller 'admin'.",
|
||||
"oauth_settings": "OAuth",
|
||||
"oauth_settings_description": "Administrer OAuth login-indstillinger",
|
||||
"oauth_settings_more_details": "Læs flere detaljer om funktionen i <link>dokumentationen</link>.",
|
||||
@@ -269,6 +271,7 @@
|
||||
"storage_template_migration_info": "Lager-skabelonen vil konvertere alle filendelser til små bogstaver. Skabelonændringer vil kun gælde for nye mediefiler. For at anvende skabelonen retroaktivt på tidligere uploadede mediefiler skal du køre <link>{job}</link>.",
|
||||
"storage_template_migration_job": "Lager Skabelon Migreringsjob",
|
||||
"storage_template_more_details": "For flere detaljer om denne funktion, referer til <template-link>Lager Skabelonen</template-link> og dens <implications-link>implikationer</implications-link>",
|
||||
"storage_template_onboarding_description_v2": "Når aktiveret, så vil denne funktion auto-organisere filer på grundlag af en brugerdefineret skabelon. For nærmere, se <link>dokumentation</link>.",
|
||||
"storage_template_path_length": "Anslået sti-længde begrænsning <b>{length, number}</b>/{limit, number}",
|
||||
"storage_template_settings": "Lagringsskabelon",
|
||||
"storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil",
|
||||
@@ -355,7 +358,9 @@
|
||||
"trash_number_of_days_description": "Antal dage aktiver i skraldespanden skal beholdes inden de fjernes permanent",
|
||||
"trash_settings": "Skraldeindstillinger",
|
||||
"trash_settings_description": "Administrér skraldeindstillinger",
|
||||
"unlink_all_oauth_accounts": "Ophæv link til alle OAuth konti",
|
||||
"unlink_all_oauth_accounts_description": "Husk at fjerne linket til alle OAuth konti før du migrerer til en ny udbyder.",
|
||||
"unlink_all_oauth_accounts_prompt": "Er du sikker på, at du vil ophæve link til alle OAuth konti? Dette vil nulstille OAuth ID for hver bruger og kan ikke fortrydes.",
|
||||
"user_cleanup_job": "Bruger-oprydning",
|
||||
"user_delete_delay": "<b>{user}</b>'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.",
|
||||
"user_delete_delay_settings": "Slet forsinkelse",
|
||||
@@ -390,7 +395,9 @@
|
||||
"advanced_settings_prefer_remote_subtitle": "Nogle enheder er meget lang tid om at indlæse miniaturebilleder af lokale elementer. Aktiver denne indstilling for at indlæse elementer fra serveren i stedet.",
|
||||
"advanced_settings_prefer_remote_title": "Foretræk elementer på serveren",
|
||||
"advanced_settings_proxy_headers_subtitle": "Definer proxy headers Immich skal sende med hver netværks forespørgsel",
|
||||
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||
"advanced_settings_proxy_headers_title": "Proxy headere",
|
||||
"advanced_settings_readonly_mode_subtitle": "Aktiverer skrivebeskyttet tilstand, hvor billederne alene kan vises. Ting som at vælge flere billeder, dele, caste og slette er alle deaktiveret. Aktiver skrivebeskyttet tilstand via en bruger avatar fra hovedskærmen",
|
||||
"advanced_settings_readonly_mode_title": "Skrivebeskyttet tilstand",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Spring verificering af SSL-certifikat over for serverens endelokation. Kræves for selvsignerede certifikater.",
|
||||
"advanced_settings_self_signed_ssl_title": "Tillad selvsignerede certifikater",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Slet eller gendan automatisk en mediefil på denne enhed, når denne handling foretages på Immich webinterface",
|
||||
@@ -406,6 +413,7 @@
|
||||
"album_cover_updated": "Albumcover opdateret",
|
||||
"album_delete_confirmation": "Er du sikker på at du vil slette albummet {album}?",
|
||||
"album_delete_confirmation_description": "Hvis dette album er delt, vil andre brugere ikke længere kunne få adgang til det.",
|
||||
"album_deleted": "Album slettet",
|
||||
"album_info_card_backup_album_excluded": "EKSKLUDERET",
|
||||
"album_info_card_backup_album_included": "INKLUDERET",
|
||||
"album_info_updated": "Albuminfo opdateret",
|
||||
@@ -415,6 +423,7 @@
|
||||
"album_options": "Albumindstillinger",
|
||||
"album_remove_user": "Fjern bruger?",
|
||||
"album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?",
|
||||
"album_search_not_found": "Ingen album fundet som matcher din søgning",
|
||||
"album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.",
|
||||
"album_updated": "Album opdateret",
|
||||
"album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler",
|
||||
@@ -434,6 +443,7 @@
|
||||
"albums_default_sort_order": "Standard album sortering",
|
||||
"albums_default_sort_order_description": "Grundlæggende sortering ved oprettelse af nyt album.",
|
||||
"albums_feature_description": "Samling af billeder der kan deles med andre brugere.",
|
||||
"albums_on_device_count": "Albummer på enheden ({count})",
|
||||
"all": "Alt",
|
||||
"all_albums": "Alle albummer",
|
||||
"all_people": "Alle personer",
|
||||
@@ -453,7 +463,9 @@
|
||||
"app_bar_signout_dialog_title": "Log ud",
|
||||
"app_settings": "Appindstillinger",
|
||||
"appears_in": "Optræder i",
|
||||
"apply_count": "Brug ({count, number})",
|
||||
"archive": "Arkiv",
|
||||
"archive_action_prompt": "{count} føjet til arkiv",
|
||||
"archive_or_unarchive_photo": "Arkivér eller dearkivér billede",
|
||||
"archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
|
||||
"archive_page_title": "Arkivér ({count})",
|
||||
@@ -488,10 +500,12 @@
|
||||
"asset_uploading": "Uploader…",
|
||||
"asset_viewer_settings_subtitle": "Administrer indstillinger for gallerifremviser",
|
||||
"asset_viewer_settings_title": "Billedviser",
|
||||
"assets": "elementer",
|
||||
"assets": "Objekter",
|
||||
"assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}",
|
||||
"assets_added_to_album_count": "{count, plural, one {# mediefil} other {# mediefiler}} tilføjet til albummet",
|
||||
"assets_added_to_albums_count": "Tilføjet {assetTotal, plural, one {# asset} other {# assets}} til {albumTotal, plural, one {# album} other {# albums}}",
|
||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Billed} other {Billeder}} kan ikke blive tilføjet til album",
|
||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} kan ikke føjes til i nogen af albummerne",
|
||||
"assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}",
|
||||
"assets_deleted_permanently": "{count} element(er) blev fjernet permanent",
|
||||
"assets_deleted_permanently_from_server": "{count} element(er) blev fjernet permanent fra Immich serveren",
|
||||
@@ -508,6 +522,7 @@
|
||||
"assets_trashed_count": "{count, plural, one {# mediefil} other {# mediefiler}} smidt i papirkurven",
|
||||
"assets_trashed_from_server": "{count} element(er) blev smidt i Immich serverens papirkurv",
|
||||
"assets_were_part_of_album_count": "mediefil{count, plural, one {mediefil} other {mediefiler}} er allerede en del af albummet",
|
||||
"assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} er allerede en del af albummerne",
|
||||
"authorized_devices": "Tilladte enheder",
|
||||
"automatic_endpoint_switching_subtitle": "Forbind lokalt over det anviste WiFi, når det er tilgængeligt og brug alternative forbindelser andre stæder",
|
||||
"automatic_endpoint_switching_title": "Automatisk skift af URL",
|
||||
@@ -572,14 +587,18 @@
|
||||
"backup_controller_page_turn_on": "Slå sikkerhedskopiering til",
|
||||
"backup_controller_page_uploading_file_info": "Uploader filinformation",
|
||||
"backup_err_only_album": "Kan ikke slette det eneste album",
|
||||
"backup_info_card_assets": "elementer",
|
||||
"backup_info_card_assets": "objekter",
|
||||
"backup_manual_cancelled": "Annulleret",
|
||||
"backup_manual_in_progress": "Upload er allerede undervejs. Prøv igen efter noget tid",
|
||||
"backup_manual_success": "Succes",
|
||||
"backup_manual_title": "Uploadstatus",
|
||||
"backup_options": "Backup indstillinger",
|
||||
"backup_options_page_title": "Backupindstillinger",
|
||||
"backup_setting_subtitle": "Administrer indstillnger for upload i forgrund og baggrund",
|
||||
"backup_settings_subtitle": "Håndtere upload indstillinger",
|
||||
"backward": "Baglæns",
|
||||
"beta_sync": "Beta synkroniseringsstatus",
|
||||
"beta_sync_subtitle": "Håndter det nye synkroniseringssystem",
|
||||
"biometric_auth_enabled": "Biometrisk adgangskontrol slået til",
|
||||
"biometric_locked_out": "Du er låst ude af biometrisk adgangskontrol",
|
||||
"biometric_no_options": "Ingen biometrisk adgangskontrol tilgængelig",
|
||||
@@ -614,6 +633,7 @@
|
||||
"cancel": "Annullér",
|
||||
"cancel_search": "Annullér søgning",
|
||||
"canceled": "Annulleret",
|
||||
"canceling": "Annullerer",
|
||||
"cannot_merge_people": "Kan ikke sammenflette personer",
|
||||
"cannot_undo_this_action": "Du kan ikke fortryde denne handling!",
|
||||
"cannot_update_the_description": "Kan ikke opdatere beskrivelsen",
|
||||
@@ -645,6 +665,7 @@
|
||||
"clear": "Ryd",
|
||||
"clear_all": "Ryd alle",
|
||||
"clear_all_recent_searches": "Ryd alle seneste søgninger",
|
||||
"clear_file_cache": "Ryd filcache",
|
||||
"clear_message": "Ryd bedsked",
|
||||
"clear_value": "Ryd værdi",
|
||||
"client_cert_dialog_msg_confirm": "OK",
|
||||
@@ -715,6 +736,7 @@
|
||||
"create_new_user": "Opret ny bruger",
|
||||
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
|
||||
"create_shared_album_page_share_select_photos": "Vælg Billeder",
|
||||
"create_shared_link": "Opret delt link",
|
||||
"create_tag": "Opret tag",
|
||||
"create_tag_description": "Opret et nyt tag. For indlejrede tags skal du indtaste den fulde sti til tagget inklusive skråstreger.",
|
||||
"create_user": "Opret bruger",
|
||||
@@ -727,9 +749,11 @@
|
||||
"current_server_address": "Nuværende serveraddresse",
|
||||
"custom_locale": "Brugerdefineret lokale",
|
||||
"custom_locale_description": "Formatér datoer og tal baseret på sproget og regionen",
|
||||
"custom_url": "Tilpasset URL",
|
||||
"daily_title_text_date": "E, dd MMM",
|
||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||
"dark": "Mørk",
|
||||
"dark_theme": "Skift til mørkt tema",
|
||||
"date_after": "Dato efter",
|
||||
"date_and_time": "Dato og klokkeslæt",
|
||||
"date_before": "Dato før",
|
||||
@@ -737,6 +761,7 @@
|
||||
"date_of_birth_saved": "Fødselsdatoen blev gemt korrekt",
|
||||
"date_range": "Datointerval",
|
||||
"day": "Dag",
|
||||
"days": "Dage",
|
||||
"deduplicate_all": "Kopier alle",
|
||||
"deduplication_criteria_1": "Billedstørrelse i bytes",
|
||||
"deduplication_criteria_2": "Antal EXIF-data",
|
||||
@@ -745,6 +770,8 @@
|
||||
"default_locale": "Standardlokalitet",
|
||||
"default_locale_description": "Formatér datoer og tal baseret på din browsers regions indstillinger",
|
||||
"delete": "Slet",
|
||||
"delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt",
|
||||
"delete_action_prompt": "{count} slettet",
|
||||
"delete_album": "Slet album",
|
||||
"delete_api_key_prompt": "Er du sikker på, at du vil slette denne API-nøgle?",
|
||||
"delete_dialog_alert": "Disse elementer vil blive slettet permanent fra Immich og din enhed",
|
||||
@@ -758,9 +785,12 @@
|
||||
"delete_key": "Slet nøgle",
|
||||
"delete_library": "Slet bibliotek",
|
||||
"delete_link": "Slet link",
|
||||
"delete_local_action_prompt": "{count} slettet lokalt",
|
||||
"delete_local_dialog_ok_backed_up_only": "Slet kun backup",
|
||||
"delete_local_dialog_ok_force": "Slet alligevel",
|
||||
"delete_others": "Slet andre",
|
||||
"delete_permanently": "Slet permanent",
|
||||
"delete_permanently_action_prompt": "{count} slettet permanent",
|
||||
"delete_shared_link": "Slet delt link",
|
||||
"delete_shared_link_dialog_title": "Slet delt link",
|
||||
"delete_tag": "Slet tag",
|
||||
@@ -771,6 +801,7 @@
|
||||
"description": "Beskrivelse",
|
||||
"description_input_hint_text": "Tilføj en beskrivelse...",
|
||||
"description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer",
|
||||
"deselect_all": "Afmarkér alt",
|
||||
"details": "DETALJER",
|
||||
"direction": "Retning",
|
||||
"disabled": "Deaktiveret",
|
||||
@@ -788,6 +819,7 @@
|
||||
"documentation": "Dokumentation",
|
||||
"done": "Færdig",
|
||||
"download": "Hent",
|
||||
"download_action_prompt": "Downloader {count} objekter",
|
||||
"download_canceled": "Download annulleret",
|
||||
"download_complete": "Download fuldført",
|
||||
"download_enqueue": "Donload sat i kø",
|
||||
@@ -814,8 +846,12 @@
|
||||
"edit": "Rediger",
|
||||
"edit_album": "Redigér album",
|
||||
"edit_avatar": "Redigér avatar",
|
||||
"edit_birthday": "Rediger fødselsdag",
|
||||
"edit_date": "Redigér dato",
|
||||
"edit_date_and_time": "Redigér dato og tid",
|
||||
"edit_date_and_time_action_prompt": "{count} dato og tid redigeret",
|
||||
"edit_date_and_time_by_offset": "Forskyde dato med offset",
|
||||
"edit_date_and_time_by_offset_interval": "Nyt datointerval: {from} - {to}",
|
||||
"edit_description": "Rediger beskrivelse",
|
||||
"edit_description_prompt": "Vælg venligst en ny beskrivelse:",
|
||||
"edit_exclusion_pattern": "Redigér udelukkelsesmønster",
|
||||
@@ -825,6 +861,7 @@
|
||||
"edit_key": "Redigér nøgle",
|
||||
"edit_link": "Rediger link",
|
||||
"edit_location": "Rediger placering",
|
||||
"edit_location_action_prompt": "{count} geolokation redigeret",
|
||||
"edit_location_dialog_title": "Placering",
|
||||
"edit_name": "Rediger navn",
|
||||
"edit_people": "Redigér personer",
|
||||
@@ -843,6 +880,7 @@
|
||||
"empty_trash": "Tøm papirkurv",
|
||||
"empty_trash_confirmation": "Er du sikker på, at du vil tømme papirkurven? Dette vil fjerne alle objekter i papirkurven permanent fra Immich.\nDu kan ikke fortryde denne handling!",
|
||||
"enable": "Aktivér",
|
||||
"enable_backup": "Aktiver backup",
|
||||
"enable_biometric_auth_description": "Indtast din PIN kode for at slå biometrisk adgangskontrol til",
|
||||
"enabled": "Aktiveret",
|
||||
"end_date": "Slutdato",
|
||||
@@ -886,6 +924,7 @@
|
||||
"failed_to_load_notifications": "Kunne ikke indlæse notifikationer",
|
||||
"failed_to_load_people": "Indlæsning af personer mislykkedes",
|
||||
"failed_to_remove_product_key": "Fjernelse af produktnøgle mislykkedes",
|
||||
"failed_to_reset_pin_code": "Kunne ikke resette PIN-koden",
|
||||
"failed_to_stack_assets": "Det lykkedes ikke at stable mediefiler",
|
||||
"failed_to_unstack_assets": "Det lykkedes ikke at fjerne gruperingen af mediefiler",
|
||||
"failed_to_update_notification_status": "Kunne ikke uploade notifikations status",
|
||||
@@ -894,6 +933,7 @@
|
||||
"paths_validation_failed": "{paths, plural, one {# sti} other {# stier}} slog fejl ved validering",
|
||||
"profile_picture_transparent_pixels": "Profilbilleder kan ikke have gennemsigtige pixels. Zoom venligst ind og/eller flyt billedet.",
|
||||
"quota_higher_than_disk_size": "Du har sat en kvote der er større end disken",
|
||||
"something_went_wrong": "Noget gik galt",
|
||||
"unable_to_add_album_users": "Ikke i stand til at tilføje brugere til album",
|
||||
"unable_to_add_assets_to_shared_link": "Kan ikke tilføje mediefiler til det delte link",
|
||||
"unable_to_add_comment": "Ikke i stand til at tilføje kommentar",
|
||||
@@ -979,6 +1019,7 @@
|
||||
},
|
||||
"exif": "Exif",
|
||||
"exif_bottom_sheet_description": "Tilføj beskrivelse...",
|
||||
"exif_bottom_sheet_description_error": "Fejl ved opdatering af beskrivelsen",
|
||||
"exif_bottom_sheet_details": "DETALJER",
|
||||
"exif_bottom_sheet_location": "LOKATION",
|
||||
"exif_bottom_sheet_people": "PERSONER",
|
||||
@@ -996,6 +1037,8 @@
|
||||
"explorer": "Udforske",
|
||||
"export": "Eksportér",
|
||||
"export_as_json": "Eksportér som JSON",
|
||||
"export_database": "Eksporter database",
|
||||
"export_database_description": "Eksporter SQLite databasen",
|
||||
"extension": "Udvidelse",
|
||||
"external": "Ekstern",
|
||||
"external_libraries": "Eksterne biblioteker",
|
||||
@@ -1007,6 +1050,7 @@
|
||||
"failed_to_load_assets": "Kunne ikke indlæse mediefiler",
|
||||
"failed_to_load_folder": "Kunne ikke indlæse mappe",
|
||||
"favorite": "Favorit",
|
||||
"favorite_action_prompt": "{count} føjet til favoritter",
|
||||
"favorite_or_unfavorite_photo": "Tilføj eller fjern fra yndlingsbilleder",
|
||||
"favorites": "Favoritter",
|
||||
"favorites_page_no_favorites": "Ingen favoritter blev fundet",
|
||||
@@ -1021,21 +1065,29 @@
|
||||
"filter_people": "Filtrér personer",
|
||||
"filter_places": "Filtrer steder",
|
||||
"find_them_fast": "Find dem hurtigt med søgning via navn",
|
||||
"first": "Første",
|
||||
"fix_incorrect_match": "Fix forkert match",
|
||||
"folder": "Mappe",
|
||||
"folder_not_found": "Mappe ikke fundet",
|
||||
"folders": "Mapper",
|
||||
"folders_feature_description": "Gennemse mappevisningen efter fotos og videoer på filsystemet",
|
||||
"forgot_pin_code_question": "Har du glemt PIN-koden?",
|
||||
"forward": "Fremad",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Denne funktion indlæser eksterne ressourcer fra Google for at virke.",
|
||||
"general": "Generel",
|
||||
"geolocation_instruction_all_have_location": "Alle objekter for denne dato har allerede lokations-data. Prøv at vise alle objekter eller vælg en anden dato",
|
||||
"geolocation_instruction_location": "Klik på et objekt med GPS-koordinater for at bruge dettes position, eller vælg position direkte på kortet",
|
||||
"geolocation_instruction_no_date": "Vælg en dato, for at administrere lokationsdata på billeder og videoer fra den dag",
|
||||
"geolocation_instruction_no_photos": "Ingen fotos eller videoer fundet for den dato. Vælg en anden dato for at vise dem",
|
||||
"get_help": "Få hjælp",
|
||||
"get_wifiname_error": "Kunne ikke hente Wi-Fi-navn. Sørg for, at du har givet de nødvendige tilladelser og er forbundet til et Wi-Fi-netværk",
|
||||
"getting_started": "Kom godt i gang",
|
||||
"go_back": "Gå tilbage",
|
||||
"go_to_folder": "Gå til mappe",
|
||||
"go_to_search": "Gå til søgning",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "Ingen GPS",
|
||||
"grant_permission": "Giv tilladelse",
|
||||
"group_albums_by": "Gruppér albummer efter...",
|
||||
"group_country": "Gruppér efter land",
|
||||
@@ -1046,6 +1098,9 @@
|
||||
"haptic_feedback_switch": "Slå haptisk feedback til",
|
||||
"haptic_feedback_title": "Haptisk feedback",
|
||||
"has_quota": "Har kvote",
|
||||
"hash_asset": "Hash objekter",
|
||||
"hashed_assets": "Hashede objekter",
|
||||
"hashing": "Hasher",
|
||||
"header_settings_add_header_tip": "Tilføj Header",
|
||||
"header_settings_field_validator_msg": "Værdi kan ikke være tom",
|
||||
"header_settings_header_name_input": "Header navn",
|
||||
@@ -1077,7 +1132,9 @@
|
||||
"home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over",
|
||||
"host": "Host",
|
||||
"hour": "Time",
|
||||
"hours": "Timer",
|
||||
"id": "ID",
|
||||
"idle": "Inaktiv",
|
||||
"ignore_icloud_photos": "Ignorer iCloud-billeder",
|
||||
"ignore_icloud_photos_description": "Billeder der er gemt på iCloud vil ikke blive uploadet til Immich-serveren",
|
||||
"image": "Billede",
|
||||
@@ -1135,10 +1192,13 @@
|
||||
"language_no_results_title": "Ingen sprog fundet",
|
||||
"language_search_hint": "Vælg sprog...",
|
||||
"language_setting_description": "Vælg dit foretrukne sprog",
|
||||
"large_files": "Store filer",
|
||||
"last": "Sidste",
|
||||
"last_seen": "Sidst set",
|
||||
"latest_version": "Seneste version",
|
||||
"latitude": "Breddegrad",
|
||||
"leave": "Forlad",
|
||||
"leave_album": "Forlad album",
|
||||
"lens_model": "Objektivmodel",
|
||||
"let_others_respond": "Lad andre svare",
|
||||
"level": "Niveau",
|
||||
@@ -1150,7 +1210,9 @@
|
||||
"library_page_sort_created": "Senest oprettet",
|
||||
"library_page_sort_last_modified": "Sidst redigeret",
|
||||
"library_page_sort_title": "Albumtitel",
|
||||
"licenses": "Licenser",
|
||||
"light": "Lys",
|
||||
"like": "Synes om",
|
||||
"like_deleted": "Ligesom slettet",
|
||||
"link_motion_video": "Link bevægelsesvideo",
|
||||
"link_to_oauth": "Link til OAuth",
|
||||
@@ -1158,7 +1220,9 @@
|
||||
"list": "Liste",
|
||||
"loading": "Indlæser",
|
||||
"loading_search_results_failed": "Indlæsning af søgeresultater fejlede",
|
||||
"local": "Lokal",
|
||||
"local_asset_cast_failed": "Kan ikke caste et aktiv, der ikke er uploadet til serveren",
|
||||
"local_assets": "Lokale objekter",
|
||||
"local_network": "Lokalt netværk",
|
||||
"local_network_sheet_info": "Appen vil oprette forbindelse til serveren via denne URL, når du bruger det angivne WiFi-netværk",
|
||||
"location_permission": "Tilladelse til placering",
|
||||
@@ -1172,6 +1236,7 @@
|
||||
"locked_folder": "Låst mappe",
|
||||
"log_out": "Log ud",
|
||||
"log_out_all_devices": "Log ud af alle enheder",
|
||||
"logged_in_as": "Logget ind som {user}",
|
||||
"logged_out_all_devices": "Logget ud af alle enheder",
|
||||
"logged_out_device": "Logget ud af enhed",
|
||||
"login": "Log ind",
|
||||
@@ -1180,7 +1245,7 @@
|
||||
"login_form_back_button_text": "Tilbage",
|
||||
"login_form_email_hint": "din-e-mail@e-mail.com",
|
||||
"login_form_endpoint_hint": "http://din-server-ip:port",
|
||||
"login_form_endpoint_url": "Server Endpoint URL",
|
||||
"login_form_endpoint_url": "Server endepunkt URL",
|
||||
"login_form_err_http": "Angiv venligst http:// eller https://",
|
||||
"login_form_err_invalid_email": "Ugyldig e-mail",
|
||||
"login_form_err_invalid_url": "Ugyldig webadresse",
|
||||
@@ -1206,6 +1271,7 @@
|
||||
"main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!",
|
||||
"main_menu": "Hovedmenu",
|
||||
"make": "Producent",
|
||||
"manage_geolocation": "Administrer placering",
|
||||
"manage_shared_links": "Håndter delte links",
|
||||
"manage_sharing_with_partners": "Administrér deling med partnere",
|
||||
"manage_the_app_settings": "Administrer appindstillinger",
|
||||
@@ -1258,6 +1324,7 @@
|
||||
"merged_people_count": "{count, plural, one {# person} other {# personer}} lagt sammen",
|
||||
"minimize": "Minimér",
|
||||
"minute": "Minut",
|
||||
"minutes": "Minutter",
|
||||
"missing": "Mangler",
|
||||
"model": "Model",
|
||||
"month": "Måned",
|
||||
@@ -1265,6 +1332,7 @@
|
||||
"more": "Mere",
|
||||
"move": "Flyt",
|
||||
"move_off_locked_folder": "Flyt ud af låst mappe",
|
||||
"move_to_lock_folder_action_prompt": "{count} føjet til i den låste mappe",
|
||||
"move_to_locked_folder": "Flyt til låst mappe",
|
||||
"move_to_locked_folder_confirmation": "Disse billeder og videoer vil blive fjernet fra alle albums, og vil kun være synlig fra den låste mappe",
|
||||
"moved_to_archive": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til arkivet",
|
||||
@@ -1276,6 +1344,9 @@
|
||||
"my_albums": "Mine albummer",
|
||||
"name": "Navn",
|
||||
"name_or_nickname": "Navn eller kælenavn",
|
||||
"network_requirement_photos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine fotos",
|
||||
"network_requirement_videos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine videoer",
|
||||
"network_requirements_updated": "Netværkskravene er ændret, backup-køen nulstilles",
|
||||
"networking_settings": "Netværk",
|
||||
"networking_subtitle": "Administrer serverens endepunktindstillinger",
|
||||
"never": "aldrig",
|
||||
@@ -1311,6 +1382,7 @@
|
||||
"no_results": "Ingen resultater",
|
||||
"no_results_description": "Prøv et synonym eller et mere generelt søgeord",
|
||||
"no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netværk",
|
||||
"no_uploads_in_progress": "Ingen upload i gang",
|
||||
"not_in_any_album": "Ikke i noget album",
|
||||
"not_selected": "Ikke valgt",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør",
|
||||
@@ -1326,6 +1398,7 @@
|
||||
"oauth": "OAuth",
|
||||
"official_immich_resources": "Officielle Immich-ressourcer",
|
||||
"offline": "Offline",
|
||||
"offset": "Forskydning",
|
||||
"ok": "Ok",
|
||||
"oldest_first": "Ældste først",
|
||||
"on_this_device": "På denne enhed",
|
||||
@@ -1348,6 +1421,7 @@
|
||||
"original": "original",
|
||||
"other": "Andet",
|
||||
"other_devices": "Andre enheder",
|
||||
"other_entities": "Andre enheder",
|
||||
"other_variables": "Andre variable",
|
||||
"owned": "Egne",
|
||||
"owner": "Ejer",
|
||||
@@ -1402,6 +1476,9 @@
|
||||
"permission_onboarding_permission_limited": "Tilladelse begrænset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.",
|
||||
"permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.",
|
||||
"person": "Person",
|
||||
"person_age_months": "{months, plural, one {# month} other {# months}} gammel",
|
||||
"person_age_year_months": "1 år, {months, plural, one {# month} other {# months}} gammel",
|
||||
"person_age_years": "{years, plural, other {# years}} gammel",
|
||||
"person_birthdate": "Født den {date}",
|
||||
"person_hidden": "{name}{hidden, select, true { (skjult)} other {}}",
|
||||
"photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller også har du ikke nogen bruger at dele med.",
|
||||
@@ -1441,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "Mobilapp er forældet. Opdater venligst til den nyeste mindre version.",
|
||||
"profile_drawer_client_server_up_to_date": "Klient og server er ajour",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "Skrivebeskyttet tilstand aktiveret. Dobbeltklik på bruger avatar ikonet for at afslutte.",
|
||||
"profile_drawer_server_out_of_date_major": "Server er forældet. Opdater venligst til den nyeste større version.",
|
||||
"profile_drawer_server_out_of_date_minor": "Server er forældet. Opdater venligst til den nyeste mindre version.",
|
||||
"profile_image_of_user": "Profilbillede af {user}",
|
||||
@@ -1476,15 +1554,18 @@
|
||||
"purchase_remove_server_product_key": "Fjern serverens produktnøgle",
|
||||
"purchase_remove_server_product_key_prompt": "Er du sikker på, at du vil fjerne serverproduktnøglen?",
|
||||
"purchase_server_description_1": "For hele serveren",
|
||||
"purchase_server_description_2": "Supporter status",
|
||||
"purchase_server_description_2": "Supporterstatus",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren",
|
||||
"queue_status": "Kø {count}/{total}",
|
||||
"rating": "Stjernebedømmelse",
|
||||
"rating_clear": "Nulstil vurdering",
|
||||
"rating_count": "{count, plural, one {# stjerne} other {# stjerner}}",
|
||||
"rating_description": "Vis EXIF-klassificeringen i infopanelet",
|
||||
"reaction_options": "Reaktionsindstillinger",
|
||||
"read_changelog": "Læs ændringslog",
|
||||
"readonly_mode_disabled": "Skrivebeskyttet tilstand deaktiveret",
|
||||
"readonly_mode_enabled": "Skrivebeskyttet tilstand aktiveret",
|
||||
"reassign": "Gentildel",
|
||||
"reassigned_assets_to_existing_person": "{count, plural, one {# mediefil} other {# mediefiler}} er blevet gentildelt til {name, select, null {en eksisterende person} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person",
|
||||
@@ -1507,6 +1588,8 @@
|
||||
"refreshing_faces": "Opdaterer ansigter",
|
||||
"refreshing_metadata": "Opdaterer metadata",
|
||||
"regenerating_thumbnails": "Regenererer forhåndsvisninger",
|
||||
"remote": "Eksternt",
|
||||
"remote_assets": "Eksterne objekter",
|
||||
"remove": "Fjern",
|
||||
"remove_assets_album_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra albummet?",
|
||||
"remove_assets_shared_link_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra dette delte link?",
|
||||
@@ -1514,7 +1597,9 @@
|
||||
"remove_custom_date_range": "Fjern tilpasset datointerval",
|
||||
"remove_deleted_assets": "Fjern slettede mediefiler",
|
||||
"remove_from_album": "Fjern fra album",
|
||||
"remove_from_album_action_prompt": "{count} fjernet fra albummet",
|
||||
"remove_from_favorites": "Fjern fra favoritter",
|
||||
"remove_from_lock_folder_action_prompt": "{count} fjernet fra den låste mappe",
|
||||
"remove_from_locked_folder": "Fjern fra låst mappe",
|
||||
"remove_from_locked_folder_confirmation": "Er du sikker på at du vil flytte disse billeder og videoer ud af den låste mappe? De vil være synlige i dit bibliotek.",
|
||||
"remove_from_shared_link": "Fjern fra delt link",
|
||||
@@ -1542,19 +1627,28 @@
|
||||
"reset_password": "Nulstil adgangskode",
|
||||
"reset_people_visibility": "Nulstil personsynlighed",
|
||||
"reset_pin_code": "Nulstil PIN kode",
|
||||
"reset_pin_code_description": "Hvis du har glemt din PIN-kode, kan du kontakte serveradministratoren for at få den stillet tilbage",
|
||||
"reset_pin_code_success": "PIN-koden er stillet tilbage",
|
||||
"reset_pin_code_with_password": "Du kan altid nulstille din PIN-kode med dit password",
|
||||
"reset_sqlite": "Reset SQLite Databasen",
|
||||
"reset_sqlite_confirmation": "Er du sikker på, at du vil nulstille SQLite databasen? Du er nødt til at logge ud og ind igen for at gensynkronisere dine data",
|
||||
"reset_sqlite_success": "Vellykket reset af SQLite databasen",
|
||||
"reset_to_default": "Nulstil til standard",
|
||||
"resolve_duplicates": "Løs dubletter",
|
||||
"resolved_all_duplicates": "Alle dubletter løst",
|
||||
"restore": "Gendan",
|
||||
"restore_all": "Gendan alle",
|
||||
"restore_trash_action_prompt": "{count} genskabt fra papirkurven",
|
||||
"restore_user": "Gendan bruger",
|
||||
"restored_asset": "Gendannet mediefilen",
|
||||
"resume": "Genoptag",
|
||||
"retry_upload": "Forsøg upload igen",
|
||||
"review_duplicates": "Gennemgå dubletter",
|
||||
"review_large_files": "Gennemgå store filer",
|
||||
"role": "Rolle",
|
||||
"role_editor": "Redaktør",
|
||||
"role_viewer": "Seer",
|
||||
"running": "Kører",
|
||||
"save": "Gem",
|
||||
"save_to_gallery": "Gem til galleri",
|
||||
"saved_api_key": "Gemt API-nøgle",
|
||||
@@ -1627,6 +1721,7 @@
|
||||
"select_album_cover": "Vælg albumcover",
|
||||
"select_all": "Vælg alle",
|
||||
"select_all_duplicates": "Vælg alle dubletter",
|
||||
"select_all_in": "Vælg alt i {group}",
|
||||
"select_avatar_color": "Vælg avatarfarve",
|
||||
"select_face": "Vælg ansigt",
|
||||
"select_featured_photo": "Vælg forsidebillede",
|
||||
@@ -1640,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album",
|
||||
"selected": "Valgt",
|
||||
"selected_count": "{count, plural, one {# valgt} other {# valgte}}",
|
||||
"selected_gps_coordinates": "Valgte GPS-koordinater",
|
||||
"send_message": "Send besked",
|
||||
"send_welcome_email": "Send velkomstemail",
|
||||
"server_endpoint": "Server endepunkt",
|
||||
@@ -1677,7 +1773,7 @@
|
||||
"setting_notifications_subtitle": "Tilpas dine notifikationspræferencer",
|
||||
"setting_notifications_total_progress_subtitle": "Samlet uploadstatus (færdige/samlet antal elementer)",
|
||||
"setting_notifications_total_progress_title": "Vis samlet baggrundsuploadstatus",
|
||||
"setting_video_viewer_looping_title": "Looping",
|
||||
"setting_video_viewer_looping_title": "Looper",
|
||||
"setting_video_viewer_original_video_subtitle": "Når der streames video fra serveren, afspil da den originale selv når en omkodet udgave er tilgængelig. Kan føre til buffering. Videoer, der er tilgængelige lokalt, afspilles i original kvalitet uanset denne indstilling.",
|
||||
"setting_video_viewer_original_video_title": "Tving original video",
|
||||
"settings": "Indstillinger",
|
||||
@@ -1685,6 +1781,7 @@
|
||||
"settings_saved": "Indstillinger er gemt",
|
||||
"setup_pin_code": "Sæt in PIN kode",
|
||||
"share": "Del",
|
||||
"share_action_prompt": "Delte {count} objekter",
|
||||
"share_add_photos": "Tilføj billeder",
|
||||
"share_assets_selected": "{count} valgt",
|
||||
"share_dialog_preparing": "Forbereder...",
|
||||
@@ -1706,6 +1803,7 @@
|
||||
"shared_link_clipboard_copied_massage": "Kopieret til udklipsholderen",
|
||||
"shared_link_clipboard_text": "Link: {link}\nAdgangskode: {password}",
|
||||
"shared_link_create_error": "Der opstod en fejl i oprettelsen af et delt link",
|
||||
"shared_link_custom_url_description": "Adgang til dette delte link med en selvdefineret URL",
|
||||
"shared_link_edit_description_hint": "Indtast beskrivelse",
|
||||
"shared_link_edit_expire_after_option_day": "1 dag",
|
||||
"shared_link_edit_expire_after_option_days": "{count} dage",
|
||||
@@ -1731,6 +1829,7 @@
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_manage_links": "Håndter delte links",
|
||||
"shared_link_options": "Muligheder for delt link",
|
||||
"shared_link_password_description": "Kræv et kodeord for at få adgang til dette delte link",
|
||||
"shared_links": "Delte links",
|
||||
"shared_links_description": "Del billeder og videoer med et link",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}",
|
||||
@@ -1747,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "tryk på ⇧ for at slette aktiv permanent",
|
||||
"show_album_options": "Vis albumindstillinger",
|
||||
"show_albums": "Vis albummer",
|
||||
"show_all_assets": "Vis alle objekter",
|
||||
"show_all_people": "Vis alle personer",
|
||||
"show_and_hide_people": "Vis & skjul personer",
|
||||
"show_assets_without_location": "Vis objekter uden lokation",
|
||||
"show_file_location": "Vis filplacering",
|
||||
"show_gallery": "Vis galleri",
|
||||
"show_hidden_people": "Vis skjulte personer",
|
||||
@@ -1780,12 +1881,14 @@
|
||||
"sort_created": "Dato oprettet",
|
||||
"sort_items": "Antal genstande",
|
||||
"sort_modified": "Ændret dato",
|
||||
"sort_newest": "Nyeste foto",
|
||||
"sort_oldest": "Ældste foto",
|
||||
"sort_people_by_similarity": "Sorter efter personer der ligner hinanden",
|
||||
"sort_recent": "Seneste foto",
|
||||
"sort_title": "Titel",
|
||||
"source": "Kilde",
|
||||
"stack": "Stak",
|
||||
"stack_action_prompt": "{count} stakket",
|
||||
"stack_duplicates": "Stak dubletter",
|
||||
"stack_select_one_photo": "Vælg ét hovedbillede til stakken",
|
||||
"stack_selected_photos": "Stak valgte billeder",
|
||||
@@ -1805,6 +1908,7 @@
|
||||
"storage_quota": "Lagringskvota",
|
||||
"storage_usage": "{used} ud af {available} brugt",
|
||||
"submit": "Indsend",
|
||||
"success": "Vellykket",
|
||||
"suggestions": "Anbefalinger",
|
||||
"sunrise_on_the_beach": "Solopgang på stranden",
|
||||
"support": "Support",
|
||||
@@ -1814,6 +1918,8 @@
|
||||
"sync": "Synkronisér",
|
||||
"sync_albums": "Synkroniser albummer",
|
||||
"sync_albums_manual_subtitle": "Synkroniser alle uploadet billeder og videoer til de valgte backupalbummer",
|
||||
"sync_local": "Synkroniser lokalt",
|
||||
"sync_remote": "Synkroniser eksternt",
|
||||
"sync_upload_album_setting_subtitle": "Opret og upload dine billeder og videoer til de valgte albummer i Immich",
|
||||
"tag": "Tag",
|
||||
"tag_assets": "Tag mediefiler",
|
||||
@@ -1824,6 +1930,7 @@
|
||||
"tag_updated": "Opdateret tag: {tag}",
|
||||
"tagged_assets": "Tagget {count, plural, one {# aktiv} other {# aktiver}}",
|
||||
"tags": "Tags",
|
||||
"tap_to_run_job": "Tryk for at køre jobbet",
|
||||
"template": "Skabelon",
|
||||
"theme": "Tema",
|
||||
"theme_selection": "Temavalg",
|
||||
@@ -1850,12 +1957,15 @@
|
||||
"to_change_password": "Skift adgangskode",
|
||||
"to_favorite": "Gør til favorit",
|
||||
"to_login": "Login",
|
||||
"to_multi_select": "For at vælge flere",
|
||||
"to_parent": "Gå op",
|
||||
"to_select": "for at vælge",
|
||||
"to_trash": "Papirkurv",
|
||||
"toggle_settings": "Slå indstillinger til eller fra",
|
||||
"total": "Total",
|
||||
"total_usage": "Samlet forbrug",
|
||||
"trash": "Papirkurv",
|
||||
"trash_action_prompt": "{count} flyttet til papirkurven",
|
||||
"trash_all": "Smid alle ud",
|
||||
"trash_count": "Slet {count, number}",
|
||||
"trash_delete_asset": "Flyt mediefil til Papirkurv",
|
||||
@@ -1873,9 +1983,11 @@
|
||||
"unable_to_change_pin_code": "Kunne ikke ændre PIN kode",
|
||||
"unable_to_setup_pin_code": "Kunne ikke sætte PIN kode",
|
||||
"unarchive": "Afakivér",
|
||||
"unarchive_action_prompt": "{count} slettet fra Arkiv",
|
||||
"unarchived_count": "{count, plural, other {Uarkiveret #}}",
|
||||
"undo": "Fortryd",
|
||||
"unfavorite": "Fjern favorit",
|
||||
"unfavorite_action_prompt": "{count} slettet fra Favoritter",
|
||||
"unhide_person": "Stop med at skjule person",
|
||||
"unknown": "Ukendt",
|
||||
"unknown_country": "Ukendt land",
|
||||
@@ -1891,16 +2003,23 @@
|
||||
"unsaved_change": "Ændring, der ikke er gemt",
|
||||
"unselect_all": "Fravælg alle",
|
||||
"unselect_all_duplicates": "Fjern markeringen af alle dubletter",
|
||||
"unselect_all_in": "Afmarkér alle i {group}",
|
||||
"unstack": "Fjern fra stak",
|
||||
"unstack_action_prompt": "{count} ustakket",
|
||||
"unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}",
|
||||
"untagged": "Umærket",
|
||||
"up_next": "Næste",
|
||||
"update_location_action_prompt": "Opdater lokationen for {count} valgte objekter med:",
|
||||
"updated_at": "Opdateret",
|
||||
"updated_password": "Opdaterede adgangskode",
|
||||
"upload": "Upload",
|
||||
"upload_action_prompt": "{count} i kø til upload",
|
||||
"upload_concurrency": "Upload samtidighed",
|
||||
"upload_details": "Upload detaljer",
|
||||
"upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?",
|
||||
"upload_dialog_title": "Upload element",
|
||||
"upload_errors": "Upload afsluttet med {count, plural, one {# fejl} other {# fejl}}. Opdater siden for at se nye uploadaktiver.",
|
||||
"upload_finished": "Upload fuldført",
|
||||
"upload_progress": "Resterende {remaining, number} - Behandlet {processed, number}/{total, number}",
|
||||
"upload_skipped_duplicates": "Sprang over {count, plural, one {# duplet aktiv} other {# duplikerede aktiver}}",
|
||||
"upload_status_duplicates": "Dubletter",
|
||||
@@ -1909,11 +2028,13 @@
|
||||
"upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.",
|
||||
"upload_to_immich": "Upload til Immich ({count})",
|
||||
"uploading": "Uploader",
|
||||
"uploading_media": "Uploader media",
|
||||
"url": "URL",
|
||||
"usage": "Forbrug",
|
||||
"use_biometric": "Brug biometrisk",
|
||||
"use_current_connection": "brug nuværende forbindelse",
|
||||
"use_custom_date_range": "Brug tilpasset datointerval i stedet",
|
||||
"use_this_location": "Klik for at benytte lokationen",
|
||||
"user": "Bruger",
|
||||
"user_has_been_deleted": "Denne bruger er slettet.",
|
||||
"user_id": "Bruger-ID",
|
||||
@@ -1929,6 +2050,7 @@
|
||||
"user_usage_stats_description": "Vis konto anvendelsesstatistik",
|
||||
"username": "Brugernavn",
|
||||
"users": "Brugere",
|
||||
"users_added_to_album_count": "Føjet {count, plural, one {# bruker} other {# brukere}} til albummet",
|
||||
"utilities": "Værktøjer",
|
||||
"validate": "Validér",
|
||||
"validate_endpoint_error": "Indtast en gyldig URL",
|
||||
@@ -1947,6 +2069,7 @@
|
||||
"view_album": "Se album",
|
||||
"view_all": "Se alle",
|
||||
"view_all_users": "Se alle brugere",
|
||||
"view_details": "Vis detaljer",
|
||||
"view_in_timeline": "Se på tidslinjen",
|
||||
"view_link": "Vis Link",
|
||||
"view_links": "Vis links",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Die Suchfilter öffnen",
|
||||
"options": "Optionen",
|
||||
"or": "oder",
|
||||
"organize_into_albums": "In Alben organisieren",
|
||||
"organize_into_albums_description": "Aktuelle Synchronisationseinstellungen verwenden, um existierende Fotos in Alben zu laden",
|
||||
"organize_your_library": "Organisiere deine Bibliothek",
|
||||
"original": "Original",
|
||||
"other": "Sonstiges",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Unterstützerstatus",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Der Server-Produktschlüssel wird durch den Administrator verwaltet",
|
||||
"query_asset_id": "Datei-ID abfragen",
|
||||
"queue_status": "Warteschlange {count}/{total}",
|
||||
"rating": "Bewertung",
|
||||
"rating_clear": "Bewertung löschen",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
|
||||
"selected": "Ausgewählt",
|
||||
"selected_count": "{count, plural, other {# ausgewählt}}",
|
||||
"selected_gps_coordinates": "Ausgewählte GPS Koordinaten",
|
||||
"selected_gps_coordinates": "Ausgewählte GPS-Koordinaten",
|
||||
"send_message": "Nachricht senden",
|
||||
"send_welcome_email": "Begrüssungsmail senden",
|
||||
"server_endpoint": "Server-Endpunkt",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Nächste Datei anzeigen",
|
||||
"view_previous_asset": "Vorherige Datei anzeigen",
|
||||
"view_qr_code": "QR code anzeigen",
|
||||
"view_similar_photos": "Zeige ähnliche Fotos an",
|
||||
"view_stack": "Stapel anzeigen",
|
||||
"view_user": "Benutzer anzeigen",
|
||||
"viewer_remove_from_stack": "Aus Stapel entfernen",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Open the search filters",
|
||||
"options": "Options",
|
||||
"or": "or",
|
||||
"organize_into_albums": "Organize into albums",
|
||||
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
|
||||
"organize_your_library": "Organize your library",
|
||||
"original": "original",
|
||||
"other": "Other",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Supporter status",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||
"query_asset_id": "Query Asset ID",
|
||||
"queue_status": "Queuing {count}/{total}",
|
||||
"rating": "Star rating",
|
||||
"rating_clear": "Clear rating",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||
"selected": "Selected",
|
||||
"selected_count": "{count, plural, other {# selected}}",
|
||||
"selected_gps_coordinates": "selected gps coordinates",
|
||||
"selected_gps_coordinates": "Selected GPS Coordinates",
|
||||
"send_message": "Send message",
|
||||
"send_welcome_email": "Send welcome email",
|
||||
"server_endpoint": "Server Endpoint",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "View next asset",
|
||||
"view_previous_asset": "View previous asset",
|
||||
"view_qr_code": "View QR code",
|
||||
"view_similar_photos": "View similar photos",
|
||||
"view_stack": "View Stack",
|
||||
"view_user": "View User",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
|
||||
14
i18n/es.json
14
i18n/es.json
@@ -167,8 +167,8 @@
|
||||
"map_settings": "Mapa",
|
||||
"map_settings_description": "Administrar la configuración del mapa",
|
||||
"map_style_description": "Dirección URL a un tema de mapa (style.json)",
|
||||
"memory_cleanup_job": "Limpieza de memoria",
|
||||
"memory_generate_job": "Generación de memoria",
|
||||
"memory_cleanup_job": "Limpieza de recuerdos",
|
||||
"memory_generate_job": "Generación de recuerdos",
|
||||
"metadata_extraction_job": "Extracción de metadatos",
|
||||
"metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras y resolución",
|
||||
"metadata_faces_import_setting": "Activar importación de caras",
|
||||
@@ -232,7 +232,7 @@
|
||||
"oauth_storage_quota_claim_description": "Fijar la cuota de almacenamiento del usuario automáticamente al valor solicitado.",
|
||||
"oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)",
|
||||
"oauth_storage_quota_default_description": "Cuota (en GiB) que se usará cuando no se solicite un valor específico.",
|
||||
"oauth_timeout": "Límite de tiempo para la solicitud",
|
||||
"oauth_timeout": "Tiempo de espera agotado para la solicitud",
|
||||
"oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos",
|
||||
"password_enable_description": "Iniciar sesión con correo electrónico y contraseña",
|
||||
"password_settings": "Contraseña de Acceso",
|
||||
@@ -1603,8 +1603,8 @@
|
||||
"remove_from_locked_folder": "Eliminar de la carpeta protegida",
|
||||
"remove_from_locked_folder_confirmation": "¿Seguro que deseas sacar estas fotos y vídeos de la carpeta protegida? Si continúas, los elementos serán visibles en tu biblioteca.",
|
||||
"remove_from_shared_link": "Eliminar desde enlace compartido",
|
||||
"remove_memory": "Quitar memoria",
|
||||
"remove_photo_from_memory": "Quitar foto de esta memoria",
|
||||
"remove_memory": "Quitar recuerdo",
|
||||
"remove_photo_from_memory": "Quitar foto de este recuerdo",
|
||||
"remove_tag": "Quitar etiqueta",
|
||||
"remove_url": "Eliminar URL",
|
||||
"remove_user": "Eliminar usuario",
|
||||
@@ -1612,8 +1612,8 @@
|
||||
"removed_from_archive": "Eliminado del archivo",
|
||||
"removed_from_favorites": "Eliminado de favoritos",
|
||||
"removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos",
|
||||
"removed_memory": "Memoria eliminada",
|
||||
"removed_photo_from_memory": "Se ha eliminado la foto de la memoria",
|
||||
"removed_memory": "Recuerdo eliminado",
|
||||
"removed_photo_from_memory": "Foto eliminada del recuerdo",
|
||||
"removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}",
|
||||
"rename": "Renombrar",
|
||||
"repair": "Reparar",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Ava otsingufiltrid",
|
||||
"options": "Valikud",
|
||||
"or": "või",
|
||||
"organize_into_albums": "Organiseeri albumitesse",
|
||||
"organize_into_albums_description": "Pane olemasolevad fotod albumitesse, kasutades jooksvaid sünkroonimise seadeid",
|
||||
"organize_your_library": "Korrasta oma kogu",
|
||||
"original": "originaal",
|
||||
"other": "Muud",
|
||||
@@ -1735,7 +1737,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Albumi lisamine ebaõnnestus",
|
||||
"selected": "Valitud",
|
||||
"selected_count": "{count, plural, other {# valitud}}",
|
||||
"selected_gps_coordinates": "valitud GPS-koordinaadid",
|
||||
"selected_gps_coordinates": "Valitud GPS-koordinaadid",
|
||||
"send_message": "Saada sõnum",
|
||||
"send_welcome_email": "Saada tervituskiri",
|
||||
"server_endpoint": "Serveri lõpp-punkt",
|
||||
@@ -2075,6 +2077,7 @@
|
||||
"view_next_asset": "Vaata järgmist üksust",
|
||||
"view_previous_asset": "Vaata eelmist üksust",
|
||||
"view_qr_code": "Vaata QR-koodi",
|
||||
"view_similar_photos": "Vaata sarnaseid fotosid",
|
||||
"view_stack": "Vaata virna",
|
||||
"view_user": "Vaata kasutajat",
|
||||
"viewer_remove_from_stack": "Eemalda virnast",
|
||||
|
||||
@@ -396,6 +396,7 @@
|
||||
"advanced_settings_prefer_remote_title": "Suosi etäkuvia",
|
||||
"advanced_settings_proxy_headers_subtitle": "Määritä välityspalvelimen otsikot(proxy headers), jotka Immichin tulisi lähettää jokaisen verkkopyynnön mukana",
|
||||
"advanced_settings_proxy_headers_title": "Välityspalvelimen otsikot",
|
||||
"advanced_settings_readonly_mode_title": "Vain luku -tila",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Ohita SSL sertifikaattivarmennus palvelimen päätepisteellä. Vaaditaan self-signed -sertifikaateissa.",
|
||||
"advanced_settings_self_signed_ssl_title": "Salli self-signed SSL -sertifikaatit",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Poista tai palauta kohde automaattisesti tällä laitteella, kun kyseinen toiminto suoritetaan verkossa",
|
||||
@@ -866,7 +867,7 @@
|
||||
"edit_title": "Muokkaa otsikkoa",
|
||||
"edit_user": "Muokkaa käyttäjää",
|
||||
"edited": "Muokattu",
|
||||
"editor": "Editori",
|
||||
"editor": "Muokkaaja",
|
||||
"editor_close_without_save_prompt": "Muutoksia ei tallenneta",
|
||||
"editor_close_without_save_title": "Suljetaanko editori?",
|
||||
"editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet",
|
||||
@@ -1633,8 +1634,8 @@
|
||||
"review_duplicates": "Tarkastele kaksoiskappaleita",
|
||||
"review_large_files": "Tarkista suuret tiedostot",
|
||||
"role": "Rooli",
|
||||
"role_editor": "Editori",
|
||||
"role_viewer": "Toistin",
|
||||
"role_editor": "Muokkaaja",
|
||||
"role_viewer": "Katsoja",
|
||||
"running": "Käynnissä",
|
||||
"save": "Tallenna",
|
||||
"save_to_gallery": "Tallenna galleriaan",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Ouvrir les filtres de recherche",
|
||||
"options": "Options",
|
||||
"or": "ou",
|
||||
"organize_into_albums": "Organiser dans des albums",
|
||||
"organize_into_albums_description": "Mettre les photos existantes dans des albums en utilisant les paramètres de synchronisation actuels",
|
||||
"organize_your_library": "Organiser votre bibliothèque",
|
||||
"original": "original",
|
||||
"other": "Autre",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Statut de contributeur",
|
||||
"purchase_server_title": "Serveur",
|
||||
"purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur",
|
||||
"query_asset_id": "ID du média requis",
|
||||
"queue_status": "{count}/{total} en file d'attente",
|
||||
"rating": "Étoile d'évaluation",
|
||||
"rating_clear": "Effacer l'évaluation",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
|
||||
"selected": "Sélectionné",
|
||||
"selected_count": "{count, plural, one {# sélectionné} other {# sélectionnés}}",
|
||||
"selected_gps_coordinates": "coordonnées GPS sélectionnées",
|
||||
"selected_gps_coordinates": "Coordonnées GPS sélectionnées",
|
||||
"send_message": "Envoyer un message",
|
||||
"send_welcome_email": "Envoyer un courriel de bienvenue",
|
||||
"server_endpoint": "Adresse du serveur",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Voir le média suivant",
|
||||
"view_previous_asset": "Voir le média précédent",
|
||||
"view_qr_code": "Voir le QR code",
|
||||
"view_similar_photos": "Voir les photos similaires",
|
||||
"view_stack": "Afficher la pile",
|
||||
"view_user": "Voir l'utilisateur",
|
||||
"viewer_remove_from_stack": "Retirer de la pile",
|
||||
|
||||
22
i18n/he.json
22
i18n/he.json
@@ -396,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "העדף תמונות מרוחקות",
|
||||
"advanced_settings_proxy_headers_subtitle": "הגדר proxy headers שהיישום צריך לשלוח עם כל בקשת רשת",
|
||||
"advanced_settings_proxy_headers_title": "כותרות פרוקסי",
|
||||
"advanced_settings_readonly_mode_subtitle": "מאפשר את מצב לקריאה בלבד בו התמונות ניתנות לצפייה בלבד, דברים כמו בחירת תמונות מרובות, שיתוף, שידור, מחיקה הם כולם מושבתים. אפשר/השבת מצב לקריאה בלבד באמצעות יצגן המשתמש מהמסך הראשי",
|
||||
"advanced_settings_readonly_mode_title": "מצב לקריאה בלבד",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית.",
|
||||
"advanced_settings_self_signed_ssl_title": "התר תעודות SSL בחתימה עצמית",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "מחק או שחזר תמונה במכשיר זה באופן אוטומטי כאשר פעולה זו נעשית בדפדפן",
|
||||
@@ -461,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "התנתק",
|
||||
"app_settings": "הגדרות יישום",
|
||||
"appears_in": "מופיע ב",
|
||||
"apply_count": "החל ({count, number})",
|
||||
"archive": "ארכיון",
|
||||
"archive_action_prompt": "{count} נוספו לארכיון",
|
||||
"archive_or_unarchive_photo": "העבר תמונה לארכיון או הוצא אותה משם",
|
||||
@@ -842,7 +845,7 @@
|
||||
"duration": "משך זמן",
|
||||
"edit": "ערוך",
|
||||
"edit_album": "ערוך אלבום",
|
||||
"edit_avatar": "ערוך תמונת פרופיל",
|
||||
"edit_avatar": "ערוך יצגן",
|
||||
"edit_birthday": "עריכת יום הולדת",
|
||||
"edit_date": "ערוך תאריך",
|
||||
"edit_date_and_time": "ערוך תאריך ושעה",
|
||||
@@ -1073,12 +1076,18 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "תכונה זאת טוענת משאבים חיצוניים מגוגל בכדי לפעול.",
|
||||
"general": "כללי",
|
||||
"geolocation_instruction_all_have_location": "לכל הפריטים עבור תאריך זה כבר יש נתוני מיקום. נסה להציג את כל הפריטים או בחר תאריך אחר",
|
||||
"geolocation_instruction_location": "לחץ על פריט עם קואורדינטות GPS כדי להשתמש במיקומו, או בחר מיקום ישירות מהמפה",
|
||||
"geolocation_instruction_no_date": "בחר תאריך כדי לנהל נתוני מיקום עבור תמונות וסרטונים מאותו יום",
|
||||
"geolocation_instruction_no_photos": "לא נמצאו תמונות או סרטונים עבור תאריך זה. בחר תאריך אחר כדי להציג אותם",
|
||||
"get_help": "קבל עזרה",
|
||||
"get_wifiname_error": "לא היה ניתן לקבל את שם האינטרנט האלחוטי שלך. יש לודא שהענקת את ההרשאות הדרושות ושאת/ה מחובר/ת לרשת אינטרנט אלחוטי",
|
||||
"getting_started": "תחילת העבודה",
|
||||
"go_back": "חזור",
|
||||
"go_to_folder": "עבור לתיקיה",
|
||||
"go_to_search": "עבור לחיפוש",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "אין GPS",
|
||||
"grant_permission": "להעניק הרשאה",
|
||||
"group_albums_by": "קבץ אלבומים לפי..",
|
||||
"group_country": "קבץ לפי מדינה",
|
||||
@@ -1262,6 +1271,7 @@
|
||||
"main_branch_warning": "הגרסה המותקנת היא גרסת פיתוח; אנחנו ממליצים בחום להשתמש בגרסה יציבה!",
|
||||
"main_menu": "תפריט ראשי",
|
||||
"make": "תוצרת",
|
||||
"manage_geolocation": "נהל מיקום",
|
||||
"manage_shared_links": "ניהול קישורים משותפים",
|
||||
"manage_sharing_with_partners": "ניהול שיתוף עם שותפים",
|
||||
"manage_the_app_settings": "ניהול הגדרות האפליקציה",
|
||||
@@ -1508,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "גרסת היישום לנייד מיושנת. נא לעדכן לגרסה המשנית האחרונה.",
|
||||
"profile_drawer_client_server_up_to_date": "היישום והשרת מעודכנים",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "מצב לקריאה בלבד מופעל. הקש הקשה כפולה על סמל היצגן של המשתמש כדי לצאת.",
|
||||
"profile_drawer_server_out_of_date_major": "השרת אינו מעודכן. נא לעדכן לגרסה הראשית האחרונה.",
|
||||
"profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה המשנית האחרונה.",
|
||||
"profile_image_of_user": "תמונת פרופיל של {user}",
|
||||
@@ -1553,6 +1564,8 @@
|
||||
"rating_description": "הצג את דירוג ה-EXIF בלוח המידע",
|
||||
"reaction_options": "אפשרויות הגבה",
|
||||
"read_changelog": "קרא את יומן השינויים",
|
||||
"readonly_mode_disabled": "מצב לקריאה בלבד מושבת",
|
||||
"readonly_mode_enabled": "מצב לקריאה בלבד מופעל",
|
||||
"reassign": "הקצה מחדש",
|
||||
"reassigned_assets_to_existing_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש אל {name, select, null {אדם קיים} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש לאדם חדש",
|
||||
@@ -1709,7 +1722,7 @@
|
||||
"select_all": "בחר הכל",
|
||||
"select_all_duplicates": "בחר את כל הכפילויות",
|
||||
"select_all_in": "בחר הכול בתוך {group}",
|
||||
"select_avatar_color": "בחר צבע תמונת פרופיל",
|
||||
"select_avatar_color": "בחר צבע יצגן",
|
||||
"select_face": "בחר פנים",
|
||||
"select_featured_photo": "בחר תמונה מייצגת",
|
||||
"select_from_computer": "בחר מהמחשב",
|
||||
@@ -1722,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "יצירת אלבום נכשלה",
|
||||
"selected": "נבחרו",
|
||||
"selected_count": "{count, plural, other {# נבחרו}}",
|
||||
"selected_gps_coordinates": "קואורדינטות GPS שנבחרו",
|
||||
"send_message": "שלח הודעה",
|
||||
"send_welcome_email": "שלח דוא\"ל קבלת פנים",
|
||||
"server_endpoint": "נקודת קצה שרת",
|
||||
@@ -1832,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "לחץ ⇧ כדי למחוק תמונה לצמיתות",
|
||||
"show_album_options": "הצג אפשרויות אלבום",
|
||||
"show_albums": "הצג אלבומים",
|
||||
"show_all_assets": "הצג את כל הפריטים",
|
||||
"show_all_people": "הצג את כל האנשים",
|
||||
"show_and_hide_people": "הצג & הסתר אנשים",
|
||||
"show_assets_without_location": "הצג פריטים ללא מיקום",
|
||||
"show_file_location": "הצג את מיקום הקובץ",
|
||||
"show_gallery": "הצג גלריה",
|
||||
"show_hidden_people": "הצג אנשים מוסתרים",
|
||||
@@ -1993,6 +2009,7 @@
|
||||
"unstacked_assets_count": "{count, plural, one {תמונה # הוסרה} other {# תמונות הוסרו}} מהערימה",
|
||||
"untagged": "לא מתיוגים",
|
||||
"up_next": "הבא בתור",
|
||||
"update_location_action_prompt": "עדכן את המיקום של {count} פריטים שנבחרו עם:",
|
||||
"updated_at": "עודכן",
|
||||
"updated_password": "סיסמה עודכנה",
|
||||
"upload": "העלאה",
|
||||
@@ -2017,6 +2034,7 @@
|
||||
"use_biometric": "השתמש באימות ביומטרי",
|
||||
"use_current_connection": "השתמש בחיבור נוכחי",
|
||||
"use_custom_date_range": "השתמש בטווח תאריכים מותאם במקום",
|
||||
"use_this_location": "לחץ כדי להשתמש במיקום",
|
||||
"user": "משתמש",
|
||||
"user_has_been_deleted": "משתמש זה נמחק.",
|
||||
"user_id": "מזהה משתמש",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"add_to_album": "Tambahkan ke album",
|
||||
"add_to_album_bottom_sheet_added": "Ditambahkan ke {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Sudah ada di {album}",
|
||||
"add_to_albums": "Tambahkan ke album",
|
||||
"add_to_shared_album": "Tambahkan ke album terbagi",
|
||||
"add_url": "Tambahkan URL",
|
||||
"added_to_archive": "Ditambahkan ke arsip",
|
||||
|
||||
22
i18n/it.json
22
i18n/it.json
@@ -392,10 +392,12 @@
|
||||
"advanced_settings_enable_alternate_media_filter_subtitle": "Usa questa opzione per filtrare i contenuti multimediali durante la sincronizzazione in base a criteri alternativi. Prova questa opzione solo se riscontri problemi con il rilevamento di tutti gli album da parte dell'app.",
|
||||
"advanced_settings_enable_alternate_media_filter_title": "[SPERIMENTALE] Usa un filtro alternativo per la sincronizzazione degli album del dispositivo",
|
||||
"advanced_settings_log_level_title": "Livello log: {level}",
|
||||
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini locali. Attivare questa impostazione per caricare invece le immagini remote.",
|
||||
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono estremamente lenti a caricare le miniature da risorse locali. Attiva questa impostazione per caricare invece le immagini remote.",
|
||||
"advanced_settings_prefer_remote_title": "Preferisci immagini remote",
|
||||
"advanced_settings_proxy_headers_subtitle": "Definisci gli header per i proxy che Immich dovrebbe inviare con ogni richiesta di rete",
|
||||
"advanced_settings_proxy_headers_title": "Header Proxy",
|
||||
"advanced_settings_readonly_mode_subtitle": "Abilita la modalità di sola lettura in cui le foto possono essere solo visualizzate, mentre funzioni come la selezione di più immagini, la condivisione, la trasmissione e l'eliminazione sono tutte disabilitate. Abilita/Disabilita la sola lettura tramite l'avatar dell'utente dalla schermata principale",
|
||||
"advanced_settings_readonly_mode_title": "Modalità di sola lettura",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.",
|
||||
"advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Rimuovi o ripristina automaticamente un elemento su questo dispositivo quando l'azione è stata fatta via web",
|
||||
@@ -461,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "Disconnetti",
|
||||
"app_settings": "Impostazioni Applicazione",
|
||||
"appears_in": "Compare in",
|
||||
"apply_count": "Applica ({count, number})",
|
||||
"archive": "Archivio",
|
||||
"archive_action_prompt": "Aggiunti {count} elementi all'Archivio",
|
||||
"archive_or_unarchive_photo": "Archivia o ripristina foto",
|
||||
@@ -1073,12 +1076,18 @@
|
||||
"gcast_enabled": "Google Cast Abilitato",
|
||||
"gcast_enabled_description": "Questa funzione carica risorse esterne da Google per poter funzionare.",
|
||||
"general": "Generale",
|
||||
"geolocation_instruction_all_have_location": "Tutte le risorse per questa data hanno già dati sulla posizione. Prova a mostrare tutte le risorse o seleziona una data diversa",
|
||||
"geolocation_instruction_location": "Fai clic su una risorsa con coordinate GPS per utilizzare la sua posizione oppure seleziona una posizione direttamente dalla mappa",
|
||||
"geolocation_instruction_no_date": "Seleziona una data per gestire i dati sulla posizione per foto e video di quel giorno",
|
||||
"geolocation_instruction_no_photos": "Nessuna foto o video trovato per questa data. Seleziona una data diversa per visualizzarli",
|
||||
"get_help": "Chiedi Aiuto",
|
||||
"get_wifiname_error": "Non sono riuscito a recuperare il nome della rete Wi-Fi. Accertati di aver concesso i permessi necessari e di essere connesso ad una rete Wi-Fi",
|
||||
"getting_started": "Iniziamo",
|
||||
"go_back": "Torna indietro",
|
||||
"go_to_folder": "Vai alla cartella",
|
||||
"go_to_search": "Vai alla ricerca",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "No GPS",
|
||||
"grant_permission": "Concedi permesso",
|
||||
"group_albums_by": "Raggruppa album in base a...",
|
||||
"group_country": "Raggruppa per paese",
|
||||
@@ -1181,7 +1190,7 @@
|
||||
"language": "Lingua",
|
||||
"language_no_results_subtitle": "Prova a cambiare i tuoi termini di ricerca",
|
||||
"language_no_results_title": "Linguaggi non trovati",
|
||||
"language_search_hint": "Cerca linguaggi...",
|
||||
"language_search_hint": "Cerca una lingua...",
|
||||
"language_setting_description": "Seleziona la tua lingua predefinita",
|
||||
"large_files": "File pesanti",
|
||||
"last": "Ultimo",
|
||||
@@ -1262,6 +1271,7 @@
|
||||
"main_branch_warning": "Stai utilizzando una versione di sviluppo. Ti consigliamo vivamente di utilizzare una versione di rilascio!",
|
||||
"main_menu": "Menu Principale",
|
||||
"make": "Produttore",
|
||||
"manage_geolocation": "Gestisci posizione",
|
||||
"manage_shared_links": "Gestisci link condivisi",
|
||||
"manage_sharing_with_partners": "Gestisci la condivisione con i compagni",
|
||||
"manage_the_app_settings": "Gestisci le impostazioni dell'applicazione",
|
||||
@@ -1508,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Aggiorna all'ultima versione minore.",
|
||||
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "Modalità di sola lettura abilitata. Tocca due volte l'icona dell'avatar dell'utente per disabilitarla.",
|
||||
"profile_drawer_server_out_of_date_major": "Il server non è aggiornato. Aggiorna all'ultima versione principale.",
|
||||
"profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Aggiorna all'ultima versione minore.",
|
||||
"profile_image_of_user": "Immagine profilo di {user}",
|
||||
@@ -1553,6 +1564,8 @@
|
||||
"rating_description": "Visualizza la valutazione EXIF nel pannello informazioni",
|
||||
"reaction_options": "Impostazioni Reazioni",
|
||||
"read_changelog": "Leggi Riepilogo Modifiche",
|
||||
"readonly_mode_disabled": "Modalità di sola lettura disabilitata",
|
||||
"readonly_mode_enabled": "Modalità di sola lettura abilitata",
|
||||
"reassign": "Riassegna",
|
||||
"reassigned_assets_to_existing_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} {name, select, null {ad una persona esistente} other {a {name}}}",
|
||||
"reassigned_assets_to_new_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} ad una nuova persona",
|
||||
@@ -1722,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Impossibile nel creare l'album",
|
||||
"selected": "Selezionato",
|
||||
"selected_count": "{count, plural, one {# selezionato} other {# selezionati}}",
|
||||
"selected_gps_coordinates": "coordinate GPS selezionate",
|
||||
"send_message": "Manda messaggio",
|
||||
"send_welcome_email": "Invia email di benvenuto",
|
||||
"server_endpoint": "Server endpoint",
|
||||
@@ -1832,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente l'asset",
|
||||
"show_album_options": "Mostra opzioni album",
|
||||
"show_albums": "Mostra gli album",
|
||||
"show_all_assets": "Mostra tutte le risorse",
|
||||
"show_all_people": "Mostra tutte le persone",
|
||||
"show_and_hide_people": "Mostra & nascondi persone",
|
||||
"show_assets_without_location": "Mostra risorse senza posizione",
|
||||
"show_file_location": "Mostra percorso file",
|
||||
"show_gallery": "Mostra galleria",
|
||||
"show_hidden_people": "Mostra persone nascoste",
|
||||
@@ -1993,6 +2009,7 @@
|
||||
"unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # asset}}",
|
||||
"untagged": "Senza tag",
|
||||
"up_next": "Prossimo",
|
||||
"update_location_action_prompt": "Aggiorna la posizione di {count} risorse selezionate con:",
|
||||
"updated_at": "Aggiornato il",
|
||||
"updated_password": "Password aggiornata",
|
||||
"upload": "Carica",
|
||||
@@ -2017,6 +2034,7 @@
|
||||
"use_biometric": "Usa biometrica",
|
||||
"use_current_connection": "usa la connessione attuale",
|
||||
"use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato",
|
||||
"use_this_location": "Clicca per usare la posizione",
|
||||
"user": "Utente",
|
||||
"user_has_been_deleted": "L'utente è stato rimosso.",
|
||||
"user_id": "ID utente",
|
||||
|
||||
@@ -392,7 +392,7 @@
|
||||
"advanced_settings_enable_alternate_media_filter_subtitle": "別の基準に従ってメディアファイルにフィルターをかけて、同期を行います。アプリがすべてのアルバムを読み込んでくれない場合にのみ、この機能を試してください。",
|
||||
"advanced_settings_enable_alternate_media_filter_title": "[試験運用] 別のデバイスのアルバム同期フィルターを使用する",
|
||||
"advanced_settings_log_level_title": "ログレベル: {level}",
|
||||
"advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションをに有効にする事により、サーバーから直接画像をロードすることが可能です。",
|
||||
"advanced_settings_prefer_remote_subtitle": "デバイスによっては、デバイス上にあるサムネイルのロードに非常に時間がかかることがあります。このオプションを有効にする事により、サーバーから直接画像をロードすることが可能です。",
|
||||
"advanced_settings_prefer_remote_title": "リモートを優先する",
|
||||
"advanced_settings_proxy_headers_subtitle": "プロキシヘッダを設定する",
|
||||
"advanced_settings_proxy_headers_title": "プロキシヘッダ",
|
||||
|
||||
@@ -1081,6 +1081,8 @@
|
||||
"go_back": "뒤로",
|
||||
"go_to_folder": "폴더로 이동",
|
||||
"go_to_search": "검색으로 이동",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "GPS 없음",
|
||||
"grant_permission": "권한 부여",
|
||||
"group_albums_by": "다음으로 앨범 그룹화...",
|
||||
"group_country": "국가별로 그룹화",
|
||||
@@ -1264,6 +1266,7 @@
|
||||
"main_branch_warning": "개발 버전을 사용 중입니다. 정식 릴리스 버전 사용을 권장합니다!",
|
||||
"main_menu": "메인 메뉴",
|
||||
"make": "제조사",
|
||||
"manage_geolocation": "위치 정보 관리",
|
||||
"manage_shared_links": "공유 링크 관리",
|
||||
"manage_sharing_with_partners": "공유할 파트너를 초대하거나 제거합니다.",
|
||||
"manage_the_app_settings": "앱 동작 및 표시 환경을 사용자 정의합니다.",
|
||||
|
||||
46
i18n/lv.json
46
i18n/lv.json
@@ -156,13 +156,18 @@
|
||||
"password_enable_description": "Pieteikšanās ar e-pasta adresi un paroli",
|
||||
"password_settings": "Pieteikšanās ar paroli",
|
||||
"password_settings_description": "Pieteikšanās ar paroli iestatījumu pārvaldība",
|
||||
"paths_validated_successfully": "Visi ceļi veiksmīgi pārbaudīti",
|
||||
"person_cleanup_job": "Personu tīrīšana",
|
||||
"quota_size_gib": "Kvotas izmērs (GiB)",
|
||||
"refreshing_all_libraries": "Atsvaidzina visas bibliotēkas",
|
||||
"registration": "Administratora reģistrācija",
|
||||
"registration_description": "Tā kā tu esi pirmais sistēmas lietotājs, tev tiks piešķirts administratora statuss un tu būsi atbildīgs par administrēšanas uzdevumiem, kā arī par citu lietotāju izveidi.",
|
||||
"require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās",
|
||||
"reset_settings_to_default": "Atjaunot iestatījumus uz noklusējuma vērtībām",
|
||||
"reset_settings_to_recent_saved": "Atjaunot iestatījumus uz pēdējiem saglabātajiem iestatījumiem",
|
||||
"scanning_library": "Skenē bibliotēku",
|
||||
"search_jobs": "Meklēt uzdevumus…",
|
||||
"send_welcome_email": "Nosūtīt sveiciena e-pastu",
|
||||
"server_external_domain_settings": "Ārējais domēns",
|
||||
"server_external_domain_settings_description": "Domēns publiski kopīgotajām saitēm, iekļaujot http(s)://",
|
||||
"server_public_users": "Publiski lietotāji",
|
||||
@@ -187,6 +192,7 @@
|
||||
"theme_custom_css_settings_description": "Cascading Style Sheets ļauj pielāgot Immich izskatu.",
|
||||
"theme_settings_description": "Immich tīmekļa saskarnes pielāgojumu pārvaldība",
|
||||
"thumbnail_generation_job": "Sīktēlu ģenerēšana",
|
||||
"thumbnail_generation_job_description": "Izveidot lielu, mazu un izplūdušu sīktēlu katram failam, kā arī sīktēlu katrai personai",
|
||||
"transcoding_acceleration_api": "Paātrināšanas API",
|
||||
"transcoding_acceleration_nvenc": "NVENC (nepieciešams NVIDIA GPU)",
|
||||
"transcoding_acceleration_qsv": "Quick Sync (nepieciešams 7. paaudzes vai jaunāks Intel procesors)",
|
||||
@@ -205,10 +211,13 @@
|
||||
"trash_number_of_days": "Dienu skaits",
|
||||
"trash_settings": "Atkritnes iestatījumi",
|
||||
"trash_settings_description": "Atkritnes iestatījumu pārvaldība",
|
||||
"user_delete_delay_settings": "Dzēšanas aizture",
|
||||
"user_delete_delay_settings_description": "Dienu skaits pēc izdzēšanas, kad neatgriezeniski tiks dzēsti lietotāja konti un faili. Lietotāju dzēšanas uzdevums tiek izpildīts pusnaktī un pārbauda, kuri lietotāji ir gatavi dzēšanai. Izmaiņas šajā iestatījumā tiks ņemtas vērā nākamajā izpildes reizē.",
|
||||
"user_delete_immediately_checkbox": "Ierindot lietotāju un failus tūlītējai dzēšanai",
|
||||
"user_details": "Lietotāja informācija",
|
||||
"user_management": "Lietotāju pārvaldība",
|
||||
"user_password_has_been_reset": "Lietotāja parole ir atiestatīta:",
|
||||
"user_password_reset_description": "Lūdzu, norādi lietotājam pagaidu paroli un informē viņu, ka nākamajā pieslēgšanās reizē viņam būs jāmaina parole.",
|
||||
"user_restore_description": "<b>{user}</b> konts tiks atjaunots.",
|
||||
"user_restore_scheduled_removal": "Atjaunot lietotāju - plānotā dzēšana {date, date, long}",
|
||||
"user_settings": "Lietotāja iestatījumi",
|
||||
@@ -238,6 +247,7 @@
|
||||
"album_added": "Albums pievienots",
|
||||
"album_added_notification_setting_description": "Saņemt e-pasta paziņojumu, kad tevi pievieno kopīgam albumam",
|
||||
"album_cover_updated": "Albuma attēls atjaunināts",
|
||||
"album_delete_confirmation_description": "Ja šis albums tiek kopīgots, citi lietotāji vairs nevarēs tam piekļūt.",
|
||||
"album_deleted": "Albums dzēsts",
|
||||
"album_info_card_backup_album_excluded": "NEIEKĻAUTS",
|
||||
"album_info_card_backup_album_included": "IEKĻAUTS",
|
||||
@@ -279,6 +289,7 @@
|
||||
"app_bar_signout_dialog_title": "Izrakstīties",
|
||||
"app_settings": "Lietotnes iestatījumi",
|
||||
"appears_in": "Parādās iekš",
|
||||
"apply_count": "Pielietot ({count, number})",
|
||||
"archive": "Arhīvs",
|
||||
"archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs",
|
||||
"archive_page_title": "Arhīvs ({count})",
|
||||
@@ -337,6 +348,7 @@
|
||||
"backup_controller_page_background_app_refresh_enable_button_text": "Doties uz iestatījumiem",
|
||||
"backup_controller_page_background_battery_info_link": "Parādīt, kā",
|
||||
"backup_controller_page_background_battery_info_message": "Lai iegūtu vislabāko fona dublēšanas pieredzi, lūdzu, atspējojiet visas akumulatora optimizācijas, kas ierobežo Immich fona aktivitāti.\n\nTā kā katrai ierīcei iestatījumi ir citādāki, lūdzu, meklējiet nepieciešamo informāciju pie ierīces ražotāja.",
|
||||
"backup_controller_page_background_battery_info_ok": "Labi",
|
||||
"backup_controller_page_background_battery_info_title": "Akumulatora optimizācija",
|
||||
"backup_controller_page_background_charging": "Tikai uzlādes laikā",
|
||||
"backup_controller_page_background_configure_error": "Neizdevās konfigurēt fona pakalpojumu",
|
||||
@@ -406,6 +418,7 @@
|
||||
"cache_settings_title": "Kešdarbes iestatījumi",
|
||||
"camera": "Fotokamera",
|
||||
"cancel": "Atcelt",
|
||||
"canceled": "Atcelts",
|
||||
"canceling": "Atceļ",
|
||||
"cannot_merge_people": "Nevar apvienot personas",
|
||||
"cast": "Pārraidīt",
|
||||
@@ -424,12 +437,19 @@
|
||||
"change_password_form_password_mismatch": "Paroles nesakrīt",
|
||||
"change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli",
|
||||
"change_pin_code": "Nomainīt PIN kodu",
|
||||
"check_corrupt_asset_backup_button": "Veikt pārbaudi",
|
||||
"choose_matching_people_to_merge": "Izvēlies atbilstošas personas apvienošanai",
|
||||
"city": "Pilsēta",
|
||||
"clear": "Notīrīt",
|
||||
"clear_all": "Notīrīt visu",
|
||||
"clear_file_cache": "Notīrīt failu kešatmiņu",
|
||||
"clear_value": "Notīrīt vērtību",
|
||||
"client_cert_dialog_msg_confirm": "Labi",
|
||||
"client_cert_enter_password": "Ievadi paroli",
|
||||
"client_cert_import": "Importēt",
|
||||
"client_cert_import_success_msg": "Klienta sertifikāts ir importēts",
|
||||
"client_cert_invalid_msg": "Nederīgs sertifikāta fails vai nepareiza parole",
|
||||
"client_cert_remove_msg": "Klienta sertifikāts ir noņemts",
|
||||
"client_cert_subtitle": "Atbalsta tikai PKCS12 (.p12, .pfx) formātu. Sertifikātu importēšana/noņemšana ir pieejama tikai pirms pieslēgšanās",
|
||||
"client_cert_title": "SSL klienta sertifikāts",
|
||||
"clockwise": "Pulksteņrādītāja virzienā",
|
||||
@@ -441,6 +461,7 @@
|
||||
"comment_deleted": "Komentārs dzēsts",
|
||||
"common_create_new_album": "Izveidot jaunu albumu",
|
||||
"common_server_error": "Lūdzu, pārbaudiet tīkla savienojumu, pārliecinieties, vai serveris ir sasniedzams un aplikācijas/servera versijas ir saderīgas.",
|
||||
"completed": "Pabeigts",
|
||||
"confirm": "Apstiprināt",
|
||||
"confirm_new_pin_code": "Apstiprināt jauno PIN kodu",
|
||||
"confirm_password": "Apstiprināt paroli",
|
||||
@@ -463,6 +484,7 @@
|
||||
"create_library": "Izveidot bibliotēku",
|
||||
"create_link": "Izveidot saiti",
|
||||
"create_link_to_share": "Izveidot kopīgošanas saiti",
|
||||
"create_new": "IZVEIDOT JAUNU",
|
||||
"create_new_person": "Izveidot jaunu personu",
|
||||
"create_new_user": "Izveidot jaunu lietotāju",
|
||||
"create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS",
|
||||
@@ -471,6 +493,7 @@
|
||||
"created_at": "Izveidots",
|
||||
"curated_object_page_title": "Lietas",
|
||||
"current_pin_code": "Esošais PIN kods",
|
||||
"current_server_address": "Pašreizējā servera adrese",
|
||||
"custom_locale": "Pielāgota lokalizācija",
|
||||
"custom_locale_description": "Formatēt datumus un skaitļus atbilstoši valodai un reģionam",
|
||||
"custom_url": "Pielāgots URL",
|
||||
@@ -524,21 +547,27 @@
|
||||
"download_action_prompt": "Lejupielādē {count} failus",
|
||||
"download_canceled": "Lejupielāde atcelta",
|
||||
"download_complete": "Lejupielāde pabeigta",
|
||||
"download_enqueue": "Lejupielāde ierindota",
|
||||
"download_error": "Lejupielādes kļūda",
|
||||
"download_failed": "Lejupielāde neizdevās",
|
||||
"download_finished": "Lejupielāde pabeigta",
|
||||
"download_notfound": "Lejupielāde nav atrasta",
|
||||
"download_paused": "Lejupielāde nopauzēta",
|
||||
"download_settings": "Lejupielāde",
|
||||
"download_settings_description": "Ar failu lejupielādi saistīto iestatījumu pārvaldība",
|
||||
"download_started": "Lejupielāde sākta",
|
||||
"download_sucess": "Lejupielāde izdevās",
|
||||
"download_sucess_android": "Multivides fails ir lejupielādēts uz DCIM/Immich",
|
||||
"download_waiting_to_retry": "Gaida, lai mēģinātu atkārtoti",
|
||||
"downloading": "Lejupielādē",
|
||||
"downloading_asset_filename": "Lejupielādē failu {filename}",
|
||||
"downloading_media": "Lejupielādē failu",
|
||||
"duplicates": "Dublikāti",
|
||||
"duplicates_description": "Atrisini katru grupu, norādot, kuri no tiem ir dublikāti",
|
||||
"duration": "Ilgums",
|
||||
"edit": "Labot",
|
||||
"edit_album": "Labot albumu",
|
||||
"edit_avatar": "Labot avatāru",
|
||||
"edit_birthday": "Labot dzimšanas dienu",
|
||||
"edit_date": "Labot datumu",
|
||||
"edit_date_and_time": "Labot datumu un laiku",
|
||||
@@ -572,6 +601,7 @@
|
||||
"enter_your_pin_code": "Ievadi savu PIN kodu",
|
||||
"enter_your_pin_code_subtitle": "Ievadi savu PIN kodu, lai piekļūtu slēgtajai mapei",
|
||||
"error": "Kļūda",
|
||||
"error_change_sort_album": "Neizdevās nomainīt albuma kārtošanas secību",
|
||||
"error_saving_image": "Kļūda: {error}",
|
||||
"errors": {
|
||||
"cant_get_faces": "Nevar iegūt sejas",
|
||||
@@ -625,10 +655,13 @@
|
||||
"export_database_description": "Eksportēt SQLite datubāzi",
|
||||
"extension": "Paplašinājums",
|
||||
"external": "Ārējs",
|
||||
"external_network": "Ārējs tīkls",
|
||||
"external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
||||
"face_unassigned": "Nepiešķirts",
|
||||
"failed": "Neizdevās",
|
||||
"failed_to_authenticate": "Neizdevās autentificēties",
|
||||
"failed_to_load_assets": "Neizdevās ielādēt failus",
|
||||
"failed_to_load_folder": "Neizdevās ielādēt mapi",
|
||||
"favorite": "Izlase",
|
||||
"favorites": "Izlase",
|
||||
"favorites_page_no_favorites": "Nav atrasti iecienītākie faili",
|
||||
@@ -654,6 +687,8 @@
|
||||
"go_back": "Doties atpakaļ",
|
||||
"go_to_folder": "Doties uz mapi",
|
||||
"go_to_search": "Doties uz meklēšanu",
|
||||
"gps": "Ir koordinātas",
|
||||
"gps_missing": "Nav koordinātu",
|
||||
"grant_permission": "Piešķirt atļauju",
|
||||
"group_albums_by": "Grupēt albumus pēc...",
|
||||
"group_country": "Grupēt pēc valsts",
|
||||
@@ -789,6 +824,7 @@
|
||||
"look": "Izskats",
|
||||
"loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaļu skatītājā.",
|
||||
"make": "Ražotājs",
|
||||
"manage_geolocation": "Pārvaldīt atrašanās vietu",
|
||||
"manage_shared_links": "Kopīgoto saišu pārvaldība",
|
||||
"manage_sharing_with_partners": "Koplietošanas ar partneriem pārvaldība",
|
||||
"manage_the_app_settings": "Lietotnes iestatījumu pārvaldība",
|
||||
@@ -856,6 +892,7 @@
|
||||
"name_or_nickname": "Vārds vai iesauka",
|
||||
"network_requirement_photos_upload": "Izmantot mobilo datu pārraidi, lai dublētu fotoattēlus",
|
||||
"network_requirement_videos_upload": "Izmantot mobilo datu pārraidi, lai dublētu video",
|
||||
"networking_subtitle": "Pārvaldīt servera galapunktu iestatījumus",
|
||||
"never": "nekad",
|
||||
"new_album": "Jauns albums",
|
||||
"new_api_key": "Jauna API atslēga",
|
||||
@@ -881,6 +918,7 @@
|
||||
"no_results": "Nav rezultātu",
|
||||
"no_results_description": "Izmēģiniet sinonīmu vai vispārīgāku atslēgvārdu",
|
||||
"not_in_any_album": "Nav nevienā albumā",
|
||||
"not_selected": "Nav izvēlēts",
|
||||
"notes": "Piezīmes",
|
||||
"nothing_here_yet": "Šeit vēl nekā nav",
|
||||
"notification_permission_dialog_content": "Lai iespējotu paziņojumus, atveriet Iestatījumi un atlasiet Atļaut.",
|
||||
@@ -1115,12 +1153,17 @@
|
||||
"see_all_people": "Skatīt visas personas",
|
||||
"select_album_cover": "Izvēlieties albuma vāciņu",
|
||||
"select_all_duplicates": "Atlasīt visus dublikātus",
|
||||
"select_avatar_color": "Izvēlies avatāra krāsu",
|
||||
"select_face": "Izvēlies seju",
|
||||
"select_from_computer": "Izvēlēties no datora",
|
||||
"select_keep_all": "Atzīmēt visus paturēšanai",
|
||||
"select_library_owner": "Izvēlies bibliotēkas īpašnieku",
|
||||
"select_new_face": "Izvēlies jaunu seju",
|
||||
"select_photos": "Fotoattēlu Izvēle",
|
||||
"select_trash_all": "Atzīmēt visus dzēšanai",
|
||||
"select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu",
|
||||
"selected": "Izvēlētie",
|
||||
"selected_gps_coordinates": "izvēlētās ģeogrāfiskās koordinātas",
|
||||
"server_info_box_app_version": "Aplikācijas Versija",
|
||||
"server_info_box_server_url": "Servera URL",
|
||||
"server_online": "Serveris tiešsaistē",
|
||||
@@ -1202,6 +1245,7 @@
|
||||
"sharing_silver_appbar_share_partner": "Dalīties ar partneri",
|
||||
"show_album_options": "Rādīt albuma iespējas",
|
||||
"show_albums": "Rādīt albumus",
|
||||
"show_all_assets": "Rādīt visus failus",
|
||||
"show_all_people": "Rādīt visas personas",
|
||||
"show_and_hide_people": "Rādīt un slēpt personas",
|
||||
"show_file_location": "Rādīt faila atrašanās vietu",
|
||||
@@ -1325,6 +1369,7 @@
|
||||
"usage": "Lietojums",
|
||||
"use_biometric": "Izmantot biometrisko autentifikāciju",
|
||||
"use_current_connection": "izmantot pašreizējo savienojumu",
|
||||
"use_custom_date_range": "Izmantot pielāgotu datuma intervālu",
|
||||
"user": "Lietotājs",
|
||||
"user_has_been_deleted": "Šis lietotājs ir dzēsts.",
|
||||
"user_id": "Lietotāja ID",
|
||||
@@ -1338,6 +1383,7 @@
|
||||
"username": "Lietotājvārds",
|
||||
"users": "Lietotāji",
|
||||
"utilities": "Rīki",
|
||||
"validate": "Pārbaudīt",
|
||||
"variables": "Mainīgie",
|
||||
"version": "Versija",
|
||||
"version_announcement_closing": "Tavs draugs, Alekss",
|
||||
|
||||
20
i18n/ml.json
20
i18n/ml.json
@@ -13,6 +13,7 @@
|
||||
"add_a_location": "ഒരു സ്ഥലം ചേർക്കുക",
|
||||
"add_a_name": "ഒരു പേര് ചേർക്കുക",
|
||||
"add_a_title": "ഒരു ശീർഷകം ചേർക്കുക",
|
||||
"add_birthday": "ജന്മദിനം ചേർക്കുക",
|
||||
"add_endpoint": "എൻഡ്പോയിന്റ് ചേർക്കുക",
|
||||
"add_exclusion_pattern": "ഒഴിവാക്കാനുള്ള മാതൃക ചേർക്കുക",
|
||||
"add_import_path": "ഇറക്കുമതി ചെയ്യുക",
|
||||
@@ -22,10 +23,12 @@
|
||||
"add_path": "പാത ചേര്ക്കുക",
|
||||
"add_photos": "ചിത്രങ്ങള് ചേര്ക്കുക",
|
||||
"add_tag": "ടാഗ് ചേര്ക്കുക",
|
||||
"add_to": "ചേര്ക്കുക",
|
||||
"add_to": "ചേര്ക്കുക…",
|
||||
"add_to_album": "ആല്ബത്തിലേക്ക് ചേര്ക്കുക",
|
||||
"add_to_album_bottom_sheet_added": "{album} - ലേക്ക് ചേര്ത്തു",
|
||||
"add_to_album_bottom_sheet_already_exists": "{album} ആൽബത്തിൽ ഇപ്പോള് തന്നെ ഉണ്ട്",
|
||||
"add_to_albums": "ആൽബങ്ങളിൽ ചേർക്കുക",
|
||||
"add_to_albums_count": "ആൽബങ്ങളിൽ ചേർക്കുക ({count})",
|
||||
"add_to_shared_album": "പങ്കിട്ട ആൽബത്തിലേക്ക് ചേർക്കുക",
|
||||
"add_url": "URL ചേര്ക്കുക",
|
||||
"added_to_archive": "ചരിത്രരേഖയായി (ആര്ക്കൈവ്) ചേര്ത്തിരിക്കുന്നു",
|
||||
@@ -68,6 +71,17 @@
|
||||
"image_format": "ഘടന",
|
||||
"image_format_description": "WebP ഉണ്ടാക്കാന് സമയം എടുക്കും എങ്കിലും JPEG ഫയലുകളെക്കാള് ചെറുതായിരിക്കും.",
|
||||
"image_fullsize_description": "അധികവിവരങ്ങള് ഒഴിവാക്കിയ ചിത്രം, വലുതാക്കി കാണിക്കുമ്പോള് ഉപയോഗിക്കപ്പെടുന്നു",
|
||||
"image_fullsize_enabled": "പൂര്ണ വലുപ്പത്തില് ഉള്ള ചിത്രങ്ങള് ഉണ്ടാക്കാന്പ്രാപ്തമാക്കുക"
|
||||
}
|
||||
"image_fullsize_enabled": "പൂര്ണ വലുപ്പത്തില് ഉള്ള ചിത്രങ്ങള് ഉണ്ടാക്കാന്പ്രാപ്തമാക്കുക",
|
||||
"image_fullsize_quality_description": "1 മുതൽ 100 വരെയുള്ള പൂർണ്ണ വലുപ്പത്തിലുള്ള ഇമേജ് നിലവാരം. ഉയർന്നതാണ് നല്ലത്, പക്ഷേ വലിയ ഫയലുകൾ ഉത്പാദിപ്പിക്കുന്നു.",
|
||||
"image_fullsize_title": "പൂർണ്ണ വലുപ്പത്തിലുള്ള ഇമേജ് ക്രമീകരണങ്ങൾ",
|
||||
"image_quality": "ഗുണനിലവാരം",
|
||||
"job_created": "ജോലി സൃഷ്ടിച്ചു",
|
||||
"job_status": "ജോലി നില"
|
||||
},
|
||||
"waiting": "കാത്തിരിക്കുന്നു",
|
||||
"warning": "മുന്നറിയിപ്പ്",
|
||||
"week": "ആഴ്ച",
|
||||
"welcome": "സ്വാഗതം",
|
||||
"year": "വർഷം",
|
||||
"yes": "അതെ"
|
||||
}
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Åpne søkefiltrene",
|
||||
"options": "Valg",
|
||||
"or": "eller",
|
||||
"organize_into_albums": "Organiser til albumer",
|
||||
"organize_into_albums_description": "Plasser eksisterende bilder i albumer ved å bruke synkroniseringsinnstillinger",
|
||||
"organize_your_library": "Organiser biblioteket ditt",
|
||||
"original": "original",
|
||||
"other": "Annet",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Støttespiller status",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren",
|
||||
"query_asset_id": "Forespør objektID",
|
||||
"queue_status": "Kø {count}/{total}",
|
||||
"rating": "Stjernevurdering",
|
||||
"rating_clear": "Slett vurdering",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Feilet ved oppretting av album",
|
||||
"selected": "Valgt",
|
||||
"selected_count": "{count, plural, other {# valgt}}",
|
||||
"selected_gps_coordinates": "valgte GPS-koordinater",
|
||||
"selected_gps_coordinates": "Valgte GPS-koordinater",
|
||||
"send_message": "Send melding",
|
||||
"send_welcome_email": "Send velkomstmelding",
|
||||
"server_endpoint": "Server endepunkt",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Vis neste fil",
|
||||
"view_previous_asset": "Vis forrige fil",
|
||||
"view_qr_code": "Vis QR-kode",
|
||||
"view_similar_photos": "Vis lignende bilder",
|
||||
"view_stack": "Vis stabel",
|
||||
"view_user": "Vis bruker",
|
||||
"viewer_remove_from_stack": "Fjern fra stabling",
|
||||
|
||||
20
i18n/nl.json
20
i18n/nl.json
@@ -396,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "Externe afbeeldingen laden",
|
||||
"advanced_settings_proxy_headers_subtitle": "Definieer proxy headers die Immich bij elk netwerkverzoek moet verzenden",
|
||||
"advanced_settings_proxy_headers_title": "Proxy headers",
|
||||
"advanced_settings_readonly_mode_subtitle": "Schakelt de alleen-lezenmodus in, waarbij de foto's alleen bekeken kunnen worden. Dingen zoals het selecteren van meerdere afbeeldingen, delen, casten en verwijderen zijn allemaal uitgeschakeld. Schakel alleen-lezen in of uit via de gebruikers avatar vanaf het hoofdscherm",
|
||||
"advanced_settings_readonly_mode_title": "Alleen-lezen Mode",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten.",
|
||||
"advanced_settings_self_signed_ssl_title": "Zelfondertekende SSL-certificaten toestaan",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatisch bestanden verwijderen of herstellen op dit apparaat als die actie op het web is ondernomen",
|
||||
@@ -461,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "Log uit",
|
||||
"app_settings": "App instellingen",
|
||||
"appears_in": "Komt voor in",
|
||||
"apply_count": "Toepassen ({count, number})",
|
||||
"archive": "Archief",
|
||||
"archive_action_prompt": "{count} item(s) toegevoegd aan het archief",
|
||||
"archive_or_unarchive_photo": "Foto archiveren of uit het archief halen",
|
||||
@@ -1073,12 +1076,18 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Deze functie gebruikt externe bronnen van Google om te kunnen werken.",
|
||||
"general": "Algemeen",
|
||||
"geolocation_instruction_all_have_location": "Alle items voor deze datum hebben al locatiegegevens. Probeer alle items te tonen of selecteer een andere datum",
|
||||
"geolocation_instruction_location": "Klik op een item met GPS coördinaten om de locatie te gebruiken, of selecteer een locatie direct vanaf de kaart",
|
||||
"geolocation_instruction_no_date": "Selecteer een datum om locatiegegevens te beheren voor foto's en video's van die dag",
|
||||
"geolocation_instruction_no_photos": "Geen foto's of video's gevonden voor deze datum. Selecteer een andere datum om ze te tonen",
|
||||
"get_help": "Krijg hulp",
|
||||
"get_wifiname_error": "Kon de WiFi-naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een WiFi-netwerk",
|
||||
"getting_started": "Aan de slag",
|
||||
"go_back": "Ga terug",
|
||||
"go_to_folder": "Ga naar map",
|
||||
"go_to_search": "Ga naar zoeken",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "Geen GPS",
|
||||
"grant_permission": "Geef toestemming",
|
||||
"group_albums_by": "Groepeer albums op...",
|
||||
"group_country": "Groepeer op land",
|
||||
@@ -1262,6 +1271,7 @@
|
||||
"main_branch_warning": "Je gebruikt een ontwikkelingsversie. We raden je ten zeerste aan een releaseversie te gebruiken!",
|
||||
"main_menu": "Hoofdmenu",
|
||||
"make": "Merk",
|
||||
"manage_geolocation": "Beheer locatie",
|
||||
"manage_shared_links": "Beheer gedeelde links",
|
||||
"manage_sharing_with_partners": "Beheer delen met partners",
|
||||
"manage_the_app_settings": "Beheer de appinstellingen",
|
||||
@@ -1508,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "Mobiele app is verouderd. Werk bij naar de nieuwste subversie.",
|
||||
"profile_drawer_client_server_up_to_date": "App en server zijn up-to-date",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "Alleen-lezen-modus ingeschakeld. Dubbeltik op het avatarpictogram van de gebruiker om te verlaten.",
|
||||
"profile_drawer_server_out_of_date_major": "Server is verouderd. Werk bij naar de nieuwste hoofdversie.",
|
||||
"profile_drawer_server_out_of_date_minor": "Server is verouderd. Werk bij naar de nieuwste subversie.",
|
||||
"profile_image_of_user": "Profielfoto van {user}",
|
||||
@@ -1553,6 +1564,8 @@
|
||||
"rating_description": "De EXIF-waardering weergeven in het infopaneel",
|
||||
"reaction_options": "Reactie-opties",
|
||||
"read_changelog": "Lees wijzigingen",
|
||||
"readonly_mode_disabled": "Alleen-lezen modus uitgeschakeld",
|
||||
"readonly_mode_enabled": "Alleen-lezen modus ingeschakeld",
|
||||
"reassign": "Opnieuw toewijzen",
|
||||
"reassigned_assets_to_existing_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan {name, select, null {een bestaand persoon} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan een nieuw persoon",
|
||||
@@ -1722,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Album aanmaken mislukt",
|
||||
"selected": "Geselecteerd",
|
||||
"selected_count": "{count, plural, other {# geselecteerd}}",
|
||||
"selected_gps_coordinates": "geselecteerde GPS coördinaten",
|
||||
"send_message": "Bericht versturen",
|
||||
"send_welcome_email": "Stuur welkomstmail",
|
||||
"server_endpoint": "Server-URL",
|
||||
@@ -1832,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "druk op ⇧ om items permanent te verwijderen",
|
||||
"show_album_options": "Toon albumopties",
|
||||
"show_albums": "Toon albums",
|
||||
"show_all_assets": "Toon alle items",
|
||||
"show_all_people": "Toon alle mensen",
|
||||
"show_and_hide_people": "Toon & verberg mensen",
|
||||
"show_assets_without_location": "Toon items zonder locatie",
|
||||
"show_file_location": "Toon bestandslocatie",
|
||||
"show_gallery": "Toon galerij",
|
||||
"show_hidden_people": "Verbogen mensen weergeven",
|
||||
@@ -1941,7 +1957,9 @@
|
||||
"to_change_password": "Wijzig wachtwoord",
|
||||
"to_favorite": "Toevoegen aan favorieten",
|
||||
"to_login": "Inloggen",
|
||||
"to_multi_select": "naar multi-select",
|
||||
"to_parent": "Ga naar hoofdmap",
|
||||
"to_select": "naar selecteren",
|
||||
"to_trash": "Prullenbak",
|
||||
"toggle_settings": "Zichtbaarheid instellingen wisselen",
|
||||
"total": "Totaal",
|
||||
@@ -1991,6 +2009,7 @@
|
||||
"unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld",
|
||||
"untagged": "Ongemarkeerd",
|
||||
"up_next": "Volgende",
|
||||
"update_location_action_prompt": "Werk de locatie bij van {count} geselecteerde items met:",
|
||||
"updated_at": "Geüpdatet",
|
||||
"updated_password": "Wachtwoord bijgewerkt",
|
||||
"upload": "Uploaden",
|
||||
@@ -2015,6 +2034,7 @@
|
||||
"use_biometric": "Gebruik biometrische authenticatie",
|
||||
"use_current_connection": "gebruik huidige verbinding",
|
||||
"use_custom_date_range": "Gebruik in plaats daarvan een aangepast datumbereik",
|
||||
"use_this_location": "Klik om locatie te gebruiken",
|
||||
"user": "Gebruiker",
|
||||
"user_has_been_deleted": "Deze gebruiker is verwijderd.",
|
||||
"user_id": "Gebruikers ID",
|
||||
|
||||
90
i18n/pl.json
90
i18n/pl.json
@@ -44,7 +44,7 @@
|
||||
"authentication_settings_description": "Zarządzaj hasłem, OAuth i innymi ustawienia uwierzytelnienia",
|
||||
"authentication_settings_disable_all": "Czy jesteś pewny, że chcesz wyłączyć wszystkie metody logowania? Logowanie będzie całkowicie wyłączone.",
|
||||
"authentication_settings_reenable": "Aby ponownie włączyć, użyj <link>Polecenia serwera</link>.",
|
||||
"background_task_job": "Zadania w Tle",
|
||||
"background_task_job": "Zadania w tle",
|
||||
"backup_database": "Utwórz Zrzut Bazy Danych",
|
||||
"backup_database_enable_description": "Włącz zrzuty bazy danych",
|
||||
"backup_keep_last_amount": "Ile poprzednich zrzutów przechowywać",
|
||||
@@ -396,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne",
|
||||
"advanced_settings_proxy_headers_subtitle": "Zdefiniuj nagłówki proxy, które Immich powinien wysyłać z każdym żądaniem sieciowym",
|
||||
"advanced_settings_proxy_headers_title": "Nagłówki proxy",
|
||||
"advanced_settings_readonly_mode_subtitle": "Włącza tryb tylko do odczytu, w którym zdjęcia można tylko przeglądać, a takie czynności jak wybieranie wielu obrazów, udostępnianie, przesyłanie i usuwanie są wyłączone. Włącz/wyłącz tryb tylko do odczytu za pomocą awatara użytkownika na ekranie głównym",
|
||||
"advanced_settings_readonly_mode_title": "Tryb tylko do odczytu",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Pomija weryfikację certyfikatu SSL dla punktu końcowego serwera. Wymagane w przypadku certyfikatów z podpisem własnym.",
|
||||
"advanced_settings_self_signed_ssl_title": "Zezwalaj na certyfikaty SSL z podpisem własnym",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatycznie usuń lub przywróć zasób na tym urządzeniu po wykonaniu tej czynności w interfejsie webowym",
|
||||
@@ -459,8 +461,9 @@
|
||||
"app_bar_signout_dialog_content": "Czy na pewno chcesz się wylogować?",
|
||||
"app_bar_signout_dialog_ok": "Tak",
|
||||
"app_bar_signout_dialog_title": "Wyloguj się",
|
||||
"app_settings": "Ustawienia Aplikacji",
|
||||
"app_settings": "Ustawienia aplikacji",
|
||||
"appears_in": "W albumach",
|
||||
"apply_count": "Zastosuj ({count, number})",
|
||||
"archive": "Archiwum",
|
||||
"archive_action_prompt": "{count} dodanych do Archiwum",
|
||||
"archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum",
|
||||
@@ -520,7 +523,7 @@
|
||||
"assets_trashed_from_server": "{count} szt. usuniętych z serwera Immich",
|
||||
"assets_were_part_of_album_count": "{count, plural, one {Zasób był} few {Zasoby były} many {Zasobów było} other {Zasobów było}} już częścią albumu",
|
||||
"assets_were_part_of_albums_count": "{count, plural, one {Zasób był} other {Zasoby były}} już częścią albumów",
|
||||
"authorized_devices": "Upoważnione Urządzenia",
|
||||
"authorized_devices": "Autoryzowane urządzenia",
|
||||
"automatic_endpoint_switching_subtitle": "Połącz się lokalnie przez wyznaczoną sieć Wi-Fi, jeśli jest dostępna, i korzystaj z alternatywnych połączeń gdzie indziej",
|
||||
"automatic_endpoint_switching_title": "Automatyczne przełączanie adresów URL",
|
||||
"autoplay_slideshow": "Automatyczne odtwarzanie pokazu slajdów",
|
||||
@@ -528,7 +531,7 @@
|
||||
"back_close_deselect": "Wróć, zamknij lub odznacz",
|
||||
"background_location_permission": "Uprawnienia do lokalizacji w tle",
|
||||
"background_location_permission_content": "Aby móc przełączać sieć podczas pracy w tle, Immich musi *zawsze* mieć dostęp do dokładnej lokalizacji, aby aplikacja mogła odczytać nazwę sieci Wi-Fi",
|
||||
"backup": "Kopia Zapasowa",
|
||||
"backup": "Kopia zapasowa",
|
||||
"backup_album_selection_page_albums_device": "Albumy na urządzeniu ({count})",
|
||||
"backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć",
|
||||
"backup_album_selection_page_assets_scatter": "Pliki mogą być rozproszone w wielu albumach. Dzięki temu albumy mogą być włączane lub wyłączane podczas procesu tworzenia kopii zapasowej.",
|
||||
@@ -538,12 +541,12 @@
|
||||
"backup_all": "Wszystkie",
|
||||
"backup_background_service_backup_failed_message": "Nie udało się wykonać kopii zapasowej zasobów. Ponowna próba…",
|
||||
"backup_background_service_connection_failed_message": "Nie udało się połączyć z serwerem. Ponowna próba…",
|
||||
"backup_background_service_current_upload_notification": "Wysyłanie {filename}",
|
||||
"backup_background_service_current_upload_notification": "Przesyłanie {filename}",
|
||||
"backup_background_service_default_notification": "Sprawdzanie nowych zasobów…",
|
||||
"backup_background_service_error_title": "Błąd kopii zapasowej",
|
||||
"backup_background_service_in_progress_notification": "Tworzenie kopii zapasowej twoich zasobów…",
|
||||
"backup_background_service_upload_failure_notification": "Błąd przesyłania {filename}",
|
||||
"backup_controller_page_albums": "Kopia Zapasowa albumów",
|
||||
"backup_controller_page_albums": "Albumy z włączoną kopią zapasową",
|
||||
"backup_controller_page_background_app_refresh_disabled_content": "Włącz odświeżanie aplikacji w tle w Ustawienia > Ogólne > Odświeżanie aplikacji w tle, aby móc korzystać z kopii zapasowej w tle.",
|
||||
"backup_controller_page_background_app_refresh_disabled_title": "Odświeżanie aplikacji w tle wyłączone",
|
||||
"backup_controller_page_background_app_refresh_enable_button_text": "Przejdź do ustawień",
|
||||
@@ -560,29 +563,29 @@
|
||||
"backup_controller_page_background_turn_off": "Wyłącz usługę w tle",
|
||||
"backup_controller_page_background_turn_on": "Włącz usługę w tle",
|
||||
"backup_controller_page_background_wifi": "Tylko Wi-Fi",
|
||||
"backup_controller_page_backup": "Kopia Zapasowa",
|
||||
"backup_controller_page_backup": "Kopia zapasowa",
|
||||
"backup_controller_page_backup_selected": "Zaznaczone: ",
|
||||
"backup_controller_page_backup_sub": "Skopiowane zdjęcia oraz filmy",
|
||||
"backup_controller_page_backup_sub": "Zdjęcia i filmy z utworzoną kopią zapasową",
|
||||
"backup_controller_page_created": "Utworzono dnia: {date}",
|
||||
"backup_controller_page_desc_backup": "Włącz kopię zapasową, aby automatycznie przesyłać nowe zasoby na serwer.",
|
||||
"backup_controller_page_desc_backup": "Włącz kopię zapasową na pierwszym planie, aby automatycznie przesyłać nowe zasoby na serwer po otworzeniu aplikacji.",
|
||||
"backup_controller_page_excluded": "Wykluczone: ",
|
||||
"backup_controller_page_failed": "Nieudane ({count})",
|
||||
"backup_controller_page_filename": "Nazwa pliku: {filename} [{size}]",
|
||||
"backup_controller_page_id": "ID: {id}",
|
||||
"backup_controller_page_info": "Informacje o kopii zapasowej",
|
||||
"backup_controller_page_none_selected": "Brak wybranych",
|
||||
"backup_controller_page_remainder": "Reszta",
|
||||
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i albumy do wykonania kopii zapasowej z wyboru",
|
||||
"backup_controller_page_remainder": "Pozostałe",
|
||||
"backup_controller_page_remainder_sub": "Pozostałe zdjęcia i filmy wybrane do wykonania kopii zapasowej",
|
||||
"backup_controller_page_server_storage": "Pamięć Serwera",
|
||||
"backup_controller_page_start_backup": "Rozpocznij Kopię Zapasową",
|
||||
"backup_controller_page_status_off": "Kopia Zapasowa jest wyłaczona",
|
||||
"backup_controller_page_status_on": "Kopia Zapasowa jest włączona",
|
||||
"backup_controller_page_storage_format": "{used} z {total} wykorzystanych",
|
||||
"backup_controller_page_to_backup": "Albumy z Kopią Zapasową",
|
||||
"backup_controller_page_status_off": "Automatyczne tworzenie kopii zapasowej na pierwszym planie jest wyłączone",
|
||||
"backup_controller_page_status_on": "Automatyczne tworzenie kopii zapasowej na pierwszym planie jest włączone",
|
||||
"backup_controller_page_storage_format": "Wykorzystano {used} z {total}",
|
||||
"backup_controller_page_to_backup": "Albumy, dla których ma być tworzona kopia zapasowa",
|
||||
"backup_controller_page_total_sub": "Wszystkie unikalne zdjęcia i filmy z wybranych albumów",
|
||||
"backup_controller_page_turn_off": "Wyłącz Kopię Zapasową",
|
||||
"backup_controller_page_turn_on": "Włącz Kopię Zapasową",
|
||||
"backup_controller_page_uploading_file_info": "Przesyłanie informacji o pliku",
|
||||
"backup_controller_page_turn_off": "Wyłącz kopię zapasową na pierwszym planie",
|
||||
"backup_controller_page_turn_on": "Włącz kopię zapasową na pierwszym planie",
|
||||
"backup_controller_page_uploading_file_info": "Informacje o przesyłanym pliku",
|
||||
"backup_err_only_album": "Nie można usunąć jedynego albumu",
|
||||
"backup_info_card_assets": "zasoby",
|
||||
"backup_manual_cancelled": "Anulowano",
|
||||
@@ -590,9 +593,9 @@
|
||||
"backup_manual_success": "Sukces",
|
||||
"backup_manual_title": "Stan przesyłania",
|
||||
"backup_options": "Opcje kopii zapasowej",
|
||||
"backup_options_page_title": "Opcje kopi zapasowej",
|
||||
"backup_options_page_title": "Opcje kopii zapasowej",
|
||||
"backup_setting_subtitle": "Zarządzaj ustawieniami przesyłania w tle i na pierwszym planie",
|
||||
"backup_settings_subtitle": "Zarządzanie ustawieniami wysyłania",
|
||||
"backup_settings_subtitle": "Zarządzanie ustawieniami przesyłania",
|
||||
"backward": "Do tyłu",
|
||||
"beta_sync": "Status synchronizacji w wersji Beta",
|
||||
"beta_sync_subtitle": "Zarządzaj nowym systemem synchronizacji",
|
||||
@@ -773,7 +776,7 @@
|
||||
"delete_api_key_prompt": "Czy na pewno chcesz usunąć ten klucz API?",
|
||||
"delete_dialog_alert": "Te elementy zostaną trwale usunięte z Immich i z Twojego urządzenia",
|
||||
"delete_dialog_alert_local": "Elementy te zostaną trwale usunięte z Twojego urządzenia, ale nadal będą dostępne na serwerze Immich",
|
||||
"delete_dialog_alert_local_non_backed_up": "Kopia zapasowa niektórych elementów nie jest tworzona w Immich i zostanie trwale usunięta z Twojego urządzenia",
|
||||
"delete_dialog_alert_local_non_backed_up": "Niektóre elementy nie mają kopii zapasowej w Immich i zostaną trwale usunięte z Twojego urządzenia",
|
||||
"delete_dialog_alert_remote": "Elementy te zostaną trwale usunięte z serwera Immich",
|
||||
"delete_dialog_ok_force": "Usuń mimo to",
|
||||
"delete_dialog_title": "Usuń trwale",
|
||||
@@ -836,7 +839,7 @@
|
||||
"downloading": "Pobieranie",
|
||||
"downloading_asset_filename": "Pobieranie zasobu {filename}",
|
||||
"downloading_media": "Pobieranie multimediów",
|
||||
"drop_files_to_upload": "Upuść pliki gdziekolwiek, aby je załadować",
|
||||
"drop_files_to_upload": "Upuść pliki w dowolnym miejscu, aby je przesłać",
|
||||
"duplicates": "Duplikaty",
|
||||
"duplicates_description": "Rozstrzygnij każdą grupę, określając, które zasoby są duplikatami, jeżeli są duplikatami",
|
||||
"duration": "Czas trwania",
|
||||
@@ -1073,12 +1076,18 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Ta funkcja , aby działać, ładuje zewnętrzne zasoby z Google.",
|
||||
"general": "Ogólne",
|
||||
"geolocation_instruction_all_have_location": "Wszystkie zasoby z tego dnia mają już dane o lokalizacji. Spróbuj wyświetlić wszystkie zasoby lub wybierz inną datę",
|
||||
"geolocation_instruction_location": "Kliknij na zasób z współrzędnymi GPS, aby użyć jego lokalizacji, lub wybierz lokalizację bezpośrednio z mapy",
|
||||
"geolocation_instruction_no_date": "Wybierz datę, aby zarządzać danymi o lokalizacji dla zdjęć i filmów z tego dnia",
|
||||
"geolocation_instruction_no_photos": "Nie znaleziono żadnych zdjęć ani filmów dla tej daty. Wybierz inną datę, aby je wyświetlić",
|
||||
"get_help": "Pomoc",
|
||||
"get_wifiname_error": "Nie można uzyskać nazwy Wi-Fi. Upewnij się, że udzieliłeś niezbędnych uprawnień i jesteś połączony z siecią Wi-Fi",
|
||||
"getting_started": "Pierwsze kroki",
|
||||
"go_back": "Wstecz",
|
||||
"go_to_folder": "Idź do folderu",
|
||||
"go_to_search": "Przejdź do wyszukiwania",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "Brak GPS",
|
||||
"grant_permission": "Udziel pozwolenia",
|
||||
"group_albums_by": "Grupuj albumy...",
|
||||
"group_country": "Grupuj według państwa",
|
||||
@@ -1186,7 +1195,7 @@
|
||||
"large_files": "Duże pliki",
|
||||
"last": "Ostatni",
|
||||
"last_seen": "Ostatnio widziane",
|
||||
"latest_version": "Najnowsza Wersja",
|
||||
"latest_version": "Najnowsza wersja",
|
||||
"latitude": "Szerokość geograficzna",
|
||||
"leave": "Opuść",
|
||||
"leave_album": "Opuść album",
|
||||
@@ -1262,6 +1271,7 @@
|
||||
"main_branch_warning": "Używasz wersji deweloperskiej. Zdecydowanie zalecamy korzystanie z wydanej wersji aplikacji!",
|
||||
"main_menu": "Menu główne",
|
||||
"make": "Marka",
|
||||
"manage_geolocation": "Zarządzaj lokalizacją",
|
||||
"manage_shared_links": "Zarządzaj udostępnionymi linkami",
|
||||
"manage_sharing_with_partners": "Zarządzaj dzieleniem z partnerami",
|
||||
"manage_the_app_settings": "Zarządzaj ustawieniami aplikacji",
|
||||
@@ -1417,17 +1427,17 @@
|
||||
"owner": "Właściciel",
|
||||
"partner": "Partner",
|
||||
"partner_can_access": "{partner} ma dostęp do",
|
||||
"partner_can_access_assets": "Twoje wszystkie zdjęcia i filmy, oprócz tych w Archiwum i Koszu",
|
||||
"partner_can_access_assets": "Wszystkie Twoje zdjęcia i filmy, oprócz tych w Archiwum i Koszu",
|
||||
"partner_can_access_location": "Informacji o tym, gdzie zostały zrobione Twoje zdjęcia",
|
||||
"partner_list_user_photos": "{user} zdjęcia",
|
||||
"partner_list_user_photos": "Zdjęcia należące do {user}",
|
||||
"partner_list_view_all": "Pokaż wszystkie",
|
||||
"partner_page_empty_message": "Twoje zdjęcia nie są udostępnione żadnemu partnerowi.",
|
||||
"partner_page_no_more_users": "Brak użytkowników do dodania",
|
||||
"partner_page_partner_add_failed": "Nie udało się dodać partnera",
|
||||
"partner_page_select_partner": "Wybierz partnera",
|
||||
"partner_page_shared_to_title": "Udostępniono",
|
||||
"partner_page_stop_sharing_content": "{partner} nie będzie już mieć dostępu do twoich zdjęć.",
|
||||
"partner_sharing": "Dzielenie z Partnerami",
|
||||
"partner_page_stop_sharing_content": "{partner} nie będzie już mieć dostępu do Twoich zdjęć.",
|
||||
"partner_sharing": "Dzielenie z partnerami",
|
||||
"partners": "Partnerzy",
|
||||
"password": "Hasło",
|
||||
"password_does_not_match": "Hasła nie są takie same",
|
||||
@@ -1504,12 +1514,13 @@
|
||||
"privacy": "Prywatność",
|
||||
"profile": "Profil",
|
||||
"profile_drawer_app_logs": "Logi",
|
||||
"profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji głównej.",
|
||||
"profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej wersji dodatkowej.",
|
||||
"profile_drawer_client_out_of_date_major": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej głównej wersji.",
|
||||
"profile_drawer_client_out_of_date_minor": "Aplikacja mobilna jest nieaktualna. Zaktualizuj do najnowszej pomniejszej wersji.",
|
||||
"profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej wersji głównej.",
|
||||
"profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej wersji dodatkowej.",
|
||||
"profile_drawer_readonly_mode": "Włączono tryb tylko do odczytu. Aby wyjść, należy dwukrotnie dotknąć ikony awatara użytkownika.",
|
||||
"profile_drawer_server_out_of_date_major": "Serwer jest nieaktualny. Zaktualizuj do najnowszej głównej wersji.",
|
||||
"profile_drawer_server_out_of_date_minor": "Serwer jest nieaktualny. Zaktualizuj do najnowszej pomniejszej wersji.",
|
||||
"profile_image_of_user": "Zdjęcie profilowe {user}",
|
||||
"profile_picture_set": "Zdjęcie profilowe ustawione.",
|
||||
"public_album": "Publiczny album",
|
||||
@@ -1553,6 +1564,8 @@
|
||||
"rating_description": "Wyświetl ocenę z EXIF w panelu informacji",
|
||||
"reaction_options": "Opcje reakcji",
|
||||
"read_changelog": "Zobacz Zmiany",
|
||||
"readonly_mode_disabled": "Tryb tylko do odczytu wyłączony",
|
||||
"readonly_mode_enabled": "Tryb tylko do odczytu włączony",
|
||||
"reassign": "Przypisz ponownie",
|
||||
"reassigned_assets_to_existing_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do {name, select, null {istniejącej osoby} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do nowej osoby",
|
||||
@@ -1722,10 +1735,11 @@
|
||||
"select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu",
|
||||
"selected": "Zaznaczone",
|
||||
"selected_count": "{count, plural, other {# wybrane}}",
|
||||
"selected_gps_coordinates": "wybrane współrzędne GPS",
|
||||
"send_message": "Wyślij wiadomość",
|
||||
"send_welcome_email": "Wyślij e-mail powitalny",
|
||||
"server_endpoint": "Punkt końcowy serwera",
|
||||
"server_info_box_app_version": "Wersja Aplikacji",
|
||||
"server_info_box_app_version": "Wersja aplikacji",
|
||||
"server_info_box_server_url": "Adres URL",
|
||||
"server_offline": "Serwer Offline",
|
||||
"server_online": "Serwer Online",
|
||||
@@ -1832,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "naciśnij ⇧, aby trwale usunąć zasób",
|
||||
"show_album_options": "Pokaż opcje albumu",
|
||||
"show_albums": "Pokaż albumy",
|
||||
"show_all_assets": "Pokaż wszystkie zasoby",
|
||||
"show_all_people": "Pokaż wszystkie osoby",
|
||||
"show_and_hide_people": "Pokaż lub ukryj osoby",
|
||||
"show_assets_without_location": "Pokaż zasoby bez lokalizacji",
|
||||
"show_file_location": "Pokaż ścieżkę pliku",
|
||||
"show_gallery": "Wyświetl galerię",
|
||||
"show_hidden_people": "Pokaż ukryte osoby",
|
||||
@@ -1885,12 +1901,12 @@
|
||||
"stop_casting": "Zatrzymaj strumieniowanie",
|
||||
"stop_motion_photo": "Zatrzymaj zdjęcie w ruchu",
|
||||
"stop_photo_sharing": "Przestać udostępniać swoje zdjęcia?",
|
||||
"stop_photo_sharing_description": "Od teraz {partner} nie będzie widzieć Twoich zdjęć.",
|
||||
"stop_photo_sharing_description": "{partner} nie będzie już mieć dostępu do Twoich zdjęć.",
|
||||
"stop_sharing_photos_with_user": "Przestań udostępniać zdjęcia temu użytkownikowi",
|
||||
"storage": "Przestrzeń dyskowa",
|
||||
"storage_label": "Etykieta magazynu",
|
||||
"storage_quota": "Limit pamięci",
|
||||
"storage_usage": "{used} z {available} użyte",
|
||||
"storage_usage": "Wykorzystano {used} z {available}",
|
||||
"submit": "Zatwierdź",
|
||||
"success": "Sukces",
|
||||
"suggestions": "Sugestie",
|
||||
@@ -1901,7 +1917,7 @@
|
||||
"swap_merge_direction": "Zmień kierunek złączenia",
|
||||
"sync": "Synchronizuj",
|
||||
"sync_albums": "Synchronizuj albumy",
|
||||
"sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami kopii zapasowych",
|
||||
"sync_albums_manual_subtitle": "Zsynchronizuj wszystkie przesłane filmy i zdjęcia z wybranymi albumami z włączoną kopią zapasową",
|
||||
"sync_local": "Synchronizacja lokalna",
|
||||
"sync_remote": "Synchronizacja zdalna",
|
||||
"sync_upload_album_setting_subtitle": "Twórz i przesyłaj swoje zdjęcia i filmy do wybranych albumów w Immich",
|
||||
@@ -1946,7 +1962,7 @@
|
||||
"to_select": "aby wybrać",
|
||||
"to_trash": "Kosz",
|
||||
"toggle_settings": "Przełącz ustawienia",
|
||||
"total": "Całkowity",
|
||||
"total": "Razem",
|
||||
"total_usage": "Całkowite wykorzystanie",
|
||||
"trash": "Kosz",
|
||||
"trash_action_prompt": "{count} przeniesione do kosza",
|
||||
@@ -1993,6 +2009,7 @@
|
||||
"unstacked_assets_count": "{count, plural, one {Rozłożony # zasób} few {Rozłożone # zasoby} other {Rozłożonych # zasobów}}",
|
||||
"untagged": "Nieoznaczone",
|
||||
"up_next": "Do następnego",
|
||||
"update_location_action_prompt": "Zaktualizuj lokalizację {count} wybranych zasobów na:",
|
||||
"updated_at": "Zaktualizowany",
|
||||
"updated_password": "Pomyślnie zaktualizowano hasło",
|
||||
"upload": "Prześlij",
|
||||
@@ -2017,6 +2034,7 @@
|
||||
"use_biometric": "Użyj biometrii",
|
||||
"use_current_connection": "użyj bieżącego połączenia",
|
||||
"use_custom_date_range": "Zamiast tego użyj niestandardowego zakresu dat",
|
||||
"use_this_location": "Kliknij, aby użyć lokalizacji",
|
||||
"user": "Użytkownik",
|
||||
"user_has_been_deleted": "Ten użytkownik został usunięty.",
|
||||
"user_id": "ID użytkownika",
|
||||
|
||||
58
i18n/pt.json
58
i18n/pt.json
@@ -28,6 +28,9 @@
|
||||
"add_to_album": "Adicionar ao álbum",
|
||||
"add_to_album_bottom_sheet_added": "Adicionado a {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Já existe em {album}",
|
||||
"add_to_album_toggle": "Alternar seleção para {album}",
|
||||
"add_to_albums": "Adicionar aos álbuns",
|
||||
"add_to_albums_count": "Adicionar aos álbuns ({count})",
|
||||
"add_to_shared_album": "Adicionar ao álbum partilhado",
|
||||
"add_url": "Adicionar URL",
|
||||
"added_to_archive": "Adicionado ao arquivo",
|
||||
@@ -355,6 +358,9 @@
|
||||
"trash_number_of_days_description": "Número de dias para manter os ficheiros na reciclagem antes de os eliminar permanentemente",
|
||||
"trash_settings": "Definições da Reciclagem",
|
||||
"trash_settings_description": "Gerir definições da reciclagem",
|
||||
"unlink_all_oauth_accounts": "Desvincular todas as contas OAuth",
|
||||
"unlink_all_oauth_accounts_description": "Lembre-se de desvincular todas as contas OAuth antes de migrar para um novo provedor.",
|
||||
"unlink_all_oauth_accounts_prompt": "Tem a certeza de que deseja desvincular todas as contas OAuth? Isso redefinirá o ID OAuth de cada utilizador e não poderá ser desfeito.",
|
||||
"user_cleanup_job": "Limpeza de utilizadores",
|
||||
"user_delete_delay": "A conta e os ficheiros de <b>{user}</b> serão agendados para eliminação permanente dentro de {delay, plural, one {# dia} other {# dias}}.",
|
||||
"user_delete_delay_settings": "Atraso de eliminação",
|
||||
@@ -390,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "Preferir imagens do servidor",
|
||||
"advanced_settings_proxy_headers_subtitle": "Defina os cabeçalhos do proxy que o Immich deve enviar em todas comunicações com a rede",
|
||||
"advanced_settings_proxy_headers_title": "Cabeçalhos do Proxy",
|
||||
"advanced_settings_readonly_mode_subtitle": "Activa o modo somente leitura, onde as fotos podem ser visualizadas. Recursos como selecionar várias imagens, partilhar, transmitir e excluir ficam deactivados. Activar/Desactivar o modo somente leitura via avatar do utilizador na janela principal",
|
||||
"advanced_settings_readonly_mode_title": "Modo somente leitura",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Não validar o certificado SSL com o endereço do servidor. Isto é necessário para certificados auto-assinados.",
|
||||
"advanced_settings_self_signed_ssl_title": "Permitir certificados SSL auto-assinados",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automaticamente eliminar ou restaurar um ficheiro neste dispositivo quando essa mesma ação for efetuada na web",
|
||||
@@ -455,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "Sair",
|
||||
"app_settings": "Definições da Aplicação",
|
||||
"appears_in": "Aparece em",
|
||||
"apply_count": "Aplicar ({count, number})",
|
||||
"archive": "Arquivo",
|
||||
"archive_action_prompt": "{count} adicionados ao Arquivo",
|
||||
"archive_or_unarchive_photo": "Arquivar ou desarquivar foto",
|
||||
@@ -494,7 +503,9 @@
|
||||
"assets": "Ficheiros",
|
||||
"assets_added_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}}",
|
||||
"assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum",
|
||||
"assets_added_to_albums_count": "Adicionado {assetTotal, plural, one {# asset} other {# assets}} a {albumTotal, plural, one {# album} other {# albums}}",
|
||||
"assets_cannot_be_added_to_album_count": "Não foi possível adicionar {count, plural, one {ficheiro} other {ficheiros}} ao álbum",
|
||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} não pode ser adicionado a nenhum dos álbuns",
|
||||
"assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}",
|
||||
"assets_deleted_permanently": "{count} ficheiro(s) eliminado(s) permanentemente",
|
||||
"assets_deleted_permanently_from_server": "{count} ficheiro(s) eliminado(s) permanentemente do servidor Immich",
|
||||
@@ -511,6 +522,7 @@
|
||||
"assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem",
|
||||
"assets_trashed_from_server": "{count} ficheiro(s) do servidor Immich foi/foram enviados para a reciclagem",
|
||||
"assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum",
|
||||
"assets_were_part_of_albums_count": "{count, plural, one {Asset was} other {Assets were}} já faz parte dos álbuns",
|
||||
"authorized_devices": "Dispositivos Autorizados",
|
||||
"automatic_endpoint_switching_subtitle": "Conecte-se localmente quando estiver em uma rede uma Wi-Fi específica e use conexões alternativas em outras redes",
|
||||
"automatic_endpoint_switching_title": "Troca automática de URL",
|
||||
@@ -749,6 +761,7 @@
|
||||
"date_of_birth_saved": "Data de nascimento guardada com sucesso",
|
||||
"date_range": "Intervalo de datas",
|
||||
"day": "Dia",
|
||||
"days": "Dias",
|
||||
"deduplicate_all": "Remover todos os duplicados",
|
||||
"deduplication_criteria_1": "Tamanho da imagem em bytes",
|
||||
"deduplication_criteria_2": "Quantidade de dados EXIF",
|
||||
@@ -833,10 +846,12 @@
|
||||
"edit": "Editar",
|
||||
"edit_album": "Editar álbum",
|
||||
"edit_avatar": "Editar imagem de perfil",
|
||||
"edit_birthday": "Alterar aniversário",
|
||||
"edit_birthday": "Editar aniversário",
|
||||
"edit_date": "Editar data",
|
||||
"edit_date_and_time": "Editar data e hora",
|
||||
"edit_date_and_time_action_prompt": "Alterada a data e hora de {count} ficheiros",
|
||||
"edit_date_and_time_by_offset": "Alterar data com diferença",
|
||||
"edit_date_and_time_by_offset_interval": "Novo período: {from} - {to}",
|
||||
"edit_description": "Editar descrição",
|
||||
"edit_description_prompt": "Por favor selecione uma nova descrição:",
|
||||
"edit_exclusion_pattern": "Editar o padrão de exclusão",
|
||||
@@ -909,6 +924,7 @@
|
||||
"failed_to_load_notifications": "Ocorreu um erro ao carregar notificações",
|
||||
"failed_to_load_people": "Ocorreu um erro ao carregar pessoas",
|
||||
"failed_to_remove_product_key": "Ocorreu um erro ao remover chave de produto",
|
||||
"failed_to_reset_pin_code": "Falha ao repor o código PIN",
|
||||
"failed_to_stack_assets": "Ocorreu um erro ao empilhar os ficheiros",
|
||||
"failed_to_unstack_assets": "Ocorreu um erro ao desempilhar ficheiros",
|
||||
"failed_to_update_notification_status": "Ocorreu um erro ao atualizar o estado das notificações",
|
||||
@@ -917,6 +933,7 @@
|
||||
"paths_validation_failed": "Ocorreu um erro na validação de {paths, plural, one {# caminho} other {# caminhos}}",
|
||||
"profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixeis transparentes. Por favor amplie e/ou mova a imagem.",
|
||||
"quota_higher_than_disk_size": "Definiu uma quota maior do que o tamanho do disco",
|
||||
"something_went_wrong": "Algo deu errado",
|
||||
"unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum",
|
||||
"unable_to_add_assets_to_shared_link": "Não foi possível adicionar os ficheiros ao link partilhado",
|
||||
"unable_to_add_comment": "Não foi possível adicionar o comentário",
|
||||
@@ -1048,21 +1065,29 @@
|
||||
"filter_people": "Filtrar pessoas",
|
||||
"filter_places": "Filtrar lugares",
|
||||
"find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa",
|
||||
"first": "Primeiro",
|
||||
"fix_incorrect_match": "Corrigir correspondência incorreta",
|
||||
"folder": "Pasta",
|
||||
"folder_not_found": "Pasta não encontrada",
|
||||
"folders": "Pastas",
|
||||
"folders_feature_description": "Navegar na vista de pastas por fotos e vídeos no sistema de ficheiros",
|
||||
"forgot_pin_code_question": "Esqueceu o seu PIN?",
|
||||
"forward": "Para a frente",
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Esta funcionalidade requer o carregamento de recursos externos da Google para poder funcionar.",
|
||||
"general": "Geral",
|
||||
"geolocation_instruction_all_have_location": "Todos os activos desta data já possuem dados de localização. Tente exibir todos os ativos ou seleccione uma data diferente",
|
||||
"geolocation_instruction_location": "Clique num ativo com coordenadas GPS para usar a sua localização ou seleccione um local diretamente do mapa",
|
||||
"geolocation_instruction_no_date": "Seleccione uma data para gerir os dados de localização de fotos e vídeos daquele dia",
|
||||
"geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado para esta data. Seleccione uma data diferente para exibi-los",
|
||||
"get_help": "Obter Ajuda",
|
||||
"get_wifiname_error": "Não foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissões necessárias e se está conectado a uma rede Wi-Fi",
|
||||
"getting_started": "Primeiros Passos",
|
||||
"go_back": "Regressar",
|
||||
"go_to_folder": "Ir para a pasta",
|
||||
"go_to_search": "Ir para a pesquisa",
|
||||
"gps": "GPS",
|
||||
"gps_missing": "Sem GPS",
|
||||
"grant_permission": "Conceder permissão",
|
||||
"group_albums_by": "Agrupar álbuns por...",
|
||||
"group_country": "Agrupar por país",
|
||||
@@ -1107,6 +1132,7 @@
|
||||
"home_page_upload_err_limit": "Só é possível enviar 30 arquivos por vez, ignorando",
|
||||
"host": "Servidor",
|
||||
"hour": "Hora",
|
||||
"hours": "Horas",
|
||||
"id": "ID",
|
||||
"idle": "Em espera",
|
||||
"ignore_icloud_photos": "ignorar fotos no iCloud",
|
||||
@@ -1167,6 +1193,7 @@
|
||||
"language_search_hint": "Procurar línguas...",
|
||||
"language_setting_description": "Selecione o seu Idioma preferido",
|
||||
"large_files": "Ficheiros Grandes",
|
||||
"last": "Último",
|
||||
"last_seen": "Visto pela ultima vez",
|
||||
"latest_version": "Versão mais recente",
|
||||
"latitude": "Latitude",
|
||||
@@ -1185,6 +1212,7 @@
|
||||
"library_page_sort_title": "Título do álbum",
|
||||
"licenses": "Licenças",
|
||||
"light": "Claro",
|
||||
"like": "Gostar",
|
||||
"like_deleted": "Gosto removido",
|
||||
"link_motion_video": "Relacionar video animado",
|
||||
"link_to_oauth": "Link do OAuth",
|
||||
@@ -1243,6 +1271,7 @@
|
||||
"main_branch_warning": "Está a utilizar uma versão de desenvolvimento, recomendamos vivamente que utilize uma versão estável!",
|
||||
"main_menu": "Menu Principal",
|
||||
"make": "Marca",
|
||||
"manage_geolocation": "Gerir localização",
|
||||
"manage_shared_links": "Gerir links partilhados",
|
||||
"manage_sharing_with_partners": "Gerir partilha com parceiros",
|
||||
"manage_the_app_settings": "Gerir definições da aplicação",
|
||||
@@ -1251,7 +1280,7 @@
|
||||
"manage_your_devices": "Gerir os seus dispositivos com sessão iniciada",
|
||||
"manage_your_oauth_connection": "Gerir a sua ligação ao OAuth",
|
||||
"map": "Mapa",
|
||||
"map_assets_in_bounds": "{count, plural, one {# foto} other {# fotos}}",
|
||||
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
||||
"map_cannot_get_user_location": "Impossível obter a sua localização",
|
||||
"map_location_dialog_yes": "Sim",
|
||||
"map_location_picker_page_use_location": "Utilizar esta localização",
|
||||
@@ -1295,6 +1324,7 @@
|
||||
"merged_people_count": "Unidas {count, plural, one {# pessoa} other {# pessoas}}",
|
||||
"minimize": "Minimizar",
|
||||
"minute": "Minuto",
|
||||
"minutes": "Minutos",
|
||||
"missing": "Em falta",
|
||||
"model": "Modelo",
|
||||
"month": "Mês",
|
||||
@@ -1314,6 +1344,9 @@
|
||||
"my_albums": "Os meus álbuns",
|
||||
"name": "Nome",
|
||||
"name_or_nickname": "Nome ou alcunha",
|
||||
"network_requirement_photos_upload": "Usar dados móveis para fazer backup de fotos",
|
||||
"network_requirement_videos_upload": "Usar dados móveis para fazer backup de vídeos",
|
||||
"network_requirements_updated": "Requisitos de rede alterados, redefinindo fila de backup",
|
||||
"networking_settings": "Conexões",
|
||||
"networking_subtitle": "Gerencie a conexão do servidor",
|
||||
"never": "Nunca",
|
||||
@@ -1365,6 +1398,7 @@
|
||||
"oauth": "OAuth",
|
||||
"official_immich_resources": "Recursos oficiais do Immich",
|
||||
"offline": "Offline",
|
||||
"offset": "Desvio",
|
||||
"ok": "Ok",
|
||||
"oldest_first": "Mais antigo primeiro",
|
||||
"on_this_device": "Neste dispositivo",
|
||||
@@ -1442,6 +1476,9 @@
|
||||
"permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça backups e gerencie sua galeria, conceda permissões para fotos e vídeos nas configurações.",
|
||||
"permission_onboarding_request": "O Immich requer autorização para ver as suas fotos e vídeos.",
|
||||
"person": "Pessoa",
|
||||
"person_age_months": "{months, plural, one {# month} other {# months}} idade",
|
||||
"person_age_year_months": "1 ano, {months, plural, one {# month} other {# months}} idade",
|
||||
"person_age_years": "{years, plural, other {# years}} idade",
|
||||
"person_birthdate": "Nasceu a {date}",
|
||||
"person_hidden": "{name}{hidden, select, true { (oculto)} other {}}",
|
||||
"photo_shared_all_users": "Parece que partilhou as suas fotos com todos os utilizadores ou não tem nenhum utilizador para partilhar.",
|
||||
@@ -1481,6 +1518,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "O aplicativo está desatualizado. Por favor, atualize para a versão mais recente.",
|
||||
"profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "Modo somente leitura activado. Toque duas vezes no ícone do avatar do utilizador para sair.",
|
||||
"profile_drawer_server_out_of_date_major": "O servidor está desatualizado. Atualize para a versão principal mais recente.",
|
||||
"profile_drawer_server_out_of_date_minor": "O servidor está desatualizado. Atualize para a versão mais recente.",
|
||||
"profile_image_of_user": "Imagem de perfil de {user}",
|
||||
@@ -1526,6 +1564,8 @@
|
||||
"rating_description": "Mostrar a classificação EXIF no painel de informações",
|
||||
"reaction_options": "Opções de reação",
|
||||
"read_changelog": "Ler Novidades",
|
||||
"readonly_mode_disabled": "Modo somente leitura desactivado",
|
||||
"readonly_mode_enabled": "Modo somente leitura activado",
|
||||
"reassign": "Reatribuir",
|
||||
"reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# ficheiro} other {# ficheiros}} para {name, select, null {uma pessoa existente} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa",
|
||||
@@ -1587,6 +1627,9 @@
|
||||
"reset_password": "Redefinir palavra-passe",
|
||||
"reset_people_visibility": "Redefinir pessoas ocultas",
|
||||
"reset_pin_code": "Repor código PIN",
|
||||
"reset_pin_code_description": "Se esqueceu o seu código PIN, pode entrar em contato com o administrador do servidor para o repor",
|
||||
"reset_pin_code_success": "Código PIN redefinido com sucesso",
|
||||
"reset_pin_code_with_password": "Pode sempre repor o seu código PIN com a sua senha",
|
||||
"reset_sqlite": "Reiniciar Base de Dados SQLite",
|
||||
"reset_sqlite_confirmation": "Tem a certeza de que quer reiniciar a base de dados SQLite? Vai ter de terminar a sessão e entrar outra vez para sincronizar os dados de novo",
|
||||
"reset_sqlite_success": "Base de dados SQLite reiniciada com sucesso",
|
||||
@@ -1601,6 +1644,7 @@
|
||||
"resume": "Continuar",
|
||||
"retry_upload": "Tentar carregar novamente",
|
||||
"review_duplicates": "Rever itens duplicados",
|
||||
"review_large_files": "Rever arquivos grandes",
|
||||
"role": "Função",
|
||||
"role_editor": "Editor",
|
||||
"role_viewer": "Visualizador",
|
||||
@@ -1691,6 +1735,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Ocorreu um erro ao criar o álbum",
|
||||
"selected": "Selecionados",
|
||||
"selected_count": "{count, plural, other {# selecionados}}",
|
||||
"selected_gps_coordinates": "coordenadas gps seleccionadas",
|
||||
"send_message": "Enviar mensagem",
|
||||
"send_welcome_email": "Enviar E-mail de boas vindas",
|
||||
"server_endpoint": "URL do servidor",
|
||||
@@ -1758,6 +1803,7 @@
|
||||
"shared_link_clipboard_copied_massage": "Copiado para a área de transferência",
|
||||
"shared_link_clipboard_text": "Ligação: {link}\nPalavra-passe: {password}",
|
||||
"shared_link_create_error": "Erro ao criar o link compartilhado",
|
||||
"shared_link_custom_url_description": "Aceda a este link partilhado com um URL personalizado",
|
||||
"shared_link_edit_description_hint": "Digite a descrição do compartilhamento",
|
||||
"shared_link_edit_expire_after_option_day": "1 dia",
|
||||
"shared_link_edit_expire_after_option_days": "{count} dias",
|
||||
@@ -1783,6 +1829,7 @@
|
||||
"shared_link_info_chip_metadata": "EXIF",
|
||||
"shared_link_manage_links": "Gerenciar links compartilhados",
|
||||
"shared_link_options": "Opções de link partilhado",
|
||||
"shared_link_password_description": "Exigir uma senha para aceder a este link partilhado",
|
||||
"shared_links": "Links partilhados",
|
||||
"shared_links_description": "Partilhar fotos e videos com um link",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos partilhados.}}",
|
||||
@@ -1799,8 +1846,10 @@
|
||||
"shift_to_permanent_delete": "Pressione ⇧ para eliminar o ficheiro permanentemente",
|
||||
"show_album_options": "Exibir opções do álbum",
|
||||
"show_albums": "Mostrar álbuns",
|
||||
"show_all_assets": "Mostrar todos os recursos",
|
||||
"show_all_people": "Mostrar todas as pessoas",
|
||||
"show_and_hide_people": "Mostrar & ocultar pessoas",
|
||||
"show_assets_without_location": "Mostrar recursos sem localização",
|
||||
"show_file_location": "Exibir localização do ficheiro",
|
||||
"show_gallery": "Exibir galeria",
|
||||
"show_hidden_people": "Exibir pessoas ocultadas",
|
||||
@@ -1832,6 +1881,7 @@
|
||||
"sort_created": "Data de criação",
|
||||
"sort_items": "Número de itens",
|
||||
"sort_modified": "Data de modificação",
|
||||
"sort_newest": "A foto mais recente",
|
||||
"sort_oldest": "Foto mais antiga",
|
||||
"sort_people_by_similarity": "Ordenar pessoas por semelhança",
|
||||
"sort_recent": "Foto mais recente",
|
||||
@@ -1907,7 +1957,9 @@
|
||||
"to_change_password": "Alterar palavra-passe",
|
||||
"to_favorite": "Favorito",
|
||||
"to_login": "Iniciar Sessão",
|
||||
"to_multi_select": "multi-selecção",
|
||||
"to_parent": "Subir um nível",
|
||||
"to_select": "seleccionar",
|
||||
"to_trash": "Reciclagem",
|
||||
"toggle_settings": "Alternar configurações",
|
||||
"total": "Total",
|
||||
@@ -1957,6 +2009,7 @@
|
||||
"unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}",
|
||||
"untagged": "Marcador removido",
|
||||
"up_next": "A seguir",
|
||||
"update_location_action_prompt": "Actualize a localização de {count} activos seleccionados com:",
|
||||
"updated_at": "Atualizado a",
|
||||
"updated_password": "Palavra-passe atualizada",
|
||||
"upload": "Carregar",
|
||||
@@ -1981,6 +2034,7 @@
|
||||
"use_biometric": "Utilizar dados biométricos",
|
||||
"use_current_connection": "usar conexão atual",
|
||||
"use_custom_date_range": "Utilizar um intervalo de datas personalizado",
|
||||
"use_this_location": "Clique para usar a localização",
|
||||
"user": "Utilizador",
|
||||
"user_has_been_deleted": "Este utilizador for eliminado.",
|
||||
"user_id": "ID do utilizador",
|
||||
|
||||
@@ -1076,8 +1076,8 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "Esta funcionalidade carrega recursos externos do Google para funcionar.",
|
||||
"general": "Geral",
|
||||
"geolocation_instruction_all_have_location": "Todos arquivos nesta data já contem dados de localização. Tente exibir todos os arquivos ou selecione uma data diferente",
|
||||
"geolocation_instruction_location": "Selecione o arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa",
|
||||
"geolocation_instruction_all_have_location": "Todos arquivos nesta data já possuem dados de localização. Tente exibir todos os arquivos ou selecione uma data diferente",
|
||||
"geolocation_instruction_location": "Selecione um arquivo com as coordenadas de GPS desejada, ou selecione a localização diretamente no mapa",
|
||||
"geolocation_instruction_no_date": "Selecione uma data para gerenciar os dados de localização das fotos e vídeos daquele dia",
|
||||
"geolocation_instruction_no_photos": "Nenhuma foto ou vídeo encontrado nesta data. Selecione uma data diferente para ser exibida",
|
||||
"get_help": "Obter Ajuda",
|
||||
|
||||
12
i18n/ro.json
12
i18n/ro.json
@@ -28,6 +28,9 @@
|
||||
"add_to_album": "Adaugă în album",
|
||||
"add_to_album_bottom_sheet_added": "Adăugat în {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Deja în {album}",
|
||||
"add_to_album_toggle": "Selectează/deselectează {album}",
|
||||
"add_to_albums": "Adaugă la albume",
|
||||
"add_to_albums_count": "Adaugă la albume ({count})",
|
||||
"add_to_shared_album": "Adaugă la album partajat",
|
||||
"add_url": "Adăugați adresa URL",
|
||||
"added_to_archive": "Adăugat la arhivă",
|
||||
@@ -181,6 +184,7 @@
|
||||
"nightly_tasks_generate_memories_setting": "Generare memorii",
|
||||
"nightly_tasks_generate_memories_setting_description": "Creează amintiri noi din resurse",
|
||||
"nightly_tasks_missing_thumbnails_setting": "Generează miniaturi lipsă",
|
||||
"nightly_tasks_missing_thumbnails_setting_description": "Pune în coadă elementele fără miniaturi pentru generarea miniaturilor",
|
||||
"nightly_tasks_settings": "Setări pentru sarcinile nocturne",
|
||||
"nightly_tasks_settings_description": "Gestionați sarcinile nocturne",
|
||||
"nightly_tasks_start_time_setting": "Ora de începere",
|
||||
@@ -354,6 +358,9 @@
|
||||
"trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ",
|
||||
"trash_settings": "Setǎri Coș de Gunoi",
|
||||
"trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi",
|
||||
"unlink_all_oauth_accounts": "Deconectează toate conturile OAuth",
|
||||
"unlink_all_oauth_accounts_description": "Nu uita să deconectezi toate conturile OAuth înainte de a migra la un nou furnizor.",
|
||||
"unlink_all_oauth_accounts_prompt": "Ești sigur că vrei să deconectezi toate conturile OAuth? Aceasta va reseta ID-ul OAuth pentru fiecare utilizator și nu poate fi anulată.",
|
||||
"user_cleanup_job": "Curățare utilizator",
|
||||
"user_delete_delay": "Contul și resursele utilizatorului <b>{user}</b> vor fi programate pentru ștergere permanentă în {delay, plural, one {# zi} other {# zile}}.",
|
||||
"user_delete_delay_settings": "Întârziere la ștergere",
|
||||
@@ -389,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "Preferă fotografii la distanță",
|
||||
"advanced_settings_proxy_headers_subtitle": "Definește antetele proxy pe care Immich ar trebui să le trimită cu fiecare solicitare de rețea",
|
||||
"advanced_settings_proxy_headers_title": "Antete Proxy",
|
||||
"advanced_settings_readonly_mode_subtitle": "Activează modul doar-citire, în care fotografiile pot fi doar vizualizate, iar acțiuni precum selectarea mai multor imagini, partajarea, redarea pe alt dispozitiv sau ștergerea sunt dezactivate. Activează/Dezactivează modul doar-citire din avatarul utilizatorului de pe ecranul principal.",
|
||||
"advanced_settings_readonly_mode_title": "Mod doar-citire",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Omite verificare certificate SSL pentru distinația server-ului, necesar pentru certificate auto-semnate.",
|
||||
"advanced_settings_self_signed_ssl_title": "Permite certificate SSL auto-semnate",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Ștergeți sau restaurați automat un element de pe acest dispozitiv atunci când acțiunea este efectuată pe web",
|
||||
@@ -454,6 +463,7 @@
|
||||
"app_bar_signout_dialog_title": "Deconectare",
|
||||
"app_settings": "Setări Aplicație",
|
||||
"appears_in": "Apare în",
|
||||
"apply_count": "Aplică ({count, number})",
|
||||
"archive": "Arhivă",
|
||||
"archive_action_prompt": "{count} adăugate la Arhivă",
|
||||
"archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia",
|
||||
@@ -493,7 +503,9 @@
|
||||
"assets": "Resurse",
|
||||
"assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}",
|
||||
"assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în album",
|
||||
"assets_added_to_albums_count": "Au fost adăugate {assetTotal, plural, one {# element} other {# elemente}} la {albumTotal, plural, one {# album} other {# albume}}",
|
||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} nu pot fi adăugate în album",
|
||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Elementul} other {Elementele}} nu poate fi adăugat la niciunul dintre albume",
|
||||
"assets_count": "{count, plural, one {# resursă} other {# resurse}}",
|
||||
"assets_deleted_permanently": "{count} poză/poze ștearsă/șterse permanent",
|
||||
"assets_deleted_permanently_from_server": "{count} poză/poze ștearsă/șterse permanent din serverul Immich",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Открыть фильтры поиска",
|
||||
"options": "Опции",
|
||||
"or": "или",
|
||||
"organize_into_albums": "Распределить по альбомам",
|
||||
"organize_into_albums_description": "Добавить уже существующие фотографии в альбомы, используя текущие настройки синхронизации",
|
||||
"organize_your_library": "Приведите в порядок свою библиотеку",
|
||||
"original": "оригинал",
|
||||
"other": "Другое",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Состояние поддержки",
|
||||
"purchase_server_title": "Сервер",
|
||||
"purchase_settings_server_activated": "Ключом продукта управляет администратор сервера",
|
||||
"query_asset_id": "Идентификатор исходного объекта",
|
||||
"queue_status": "В очереди {count}/{total}",
|
||||
"rating": "Рейтинг звёзд",
|
||||
"rating_clear": "Очистить рейтинг",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Не удалось создать альбом",
|
||||
"selected": "Выбрано",
|
||||
"selected_count": "{count, plural, one {Выбран # объект} many {Выбрано # объектов} other {Выбрано # объекта}}",
|
||||
"selected_gps_coordinates": "выбранные координаты",
|
||||
"selected_gps_coordinates": "Выбранные координаты",
|
||||
"send_message": "Отправить сообщение",
|
||||
"send_welcome_email": "Отправить приветственное письмо",
|
||||
"server_endpoint": "Адрес сервера",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Показать следующий объект",
|
||||
"view_previous_asset": "Показать предыдущий объект",
|
||||
"view_qr_code": "Посмотреть QR код",
|
||||
"view_similar_photos": "Найти похожие фотографии",
|
||||
"view_stack": "Показать группу",
|
||||
"view_user": "Просмотреть пользователя",
|
||||
"viewer_remove_from_stack": "Убрать из группы",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Otvoriť vyhľadávacie filtre",
|
||||
"options": "Nastavenia",
|
||||
"or": "alebo",
|
||||
"organize_into_albums": "Usporiadať do albumov",
|
||||
"organize_into_albums_description": "Vložiť existujúce fotky do albumov podľa aktuálnych nastavení synchronizácie",
|
||||
"organize_your_library": "Usporiadajte svoju knižnicu",
|
||||
"original": "originál",
|
||||
"other": "Ostatné",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Štatút podporovateľa",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Produktový kľúč servera spravuje admin",
|
||||
"query_asset_id": "ID požiadavky položky",
|
||||
"queue_status": "V poradí {count}/{total}",
|
||||
"rating": "Hodnotenie hviezdičkami",
|
||||
"rating_clear": "Vyčistiť hodnotenie",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album",
|
||||
"selected": "Vybrané",
|
||||
"selected_count": "{count, plural, one {# vybraná} few {# vybrané} other {# vybraných}}",
|
||||
"selected_gps_coordinates": "vybrané GPS súradnice",
|
||||
"selected_gps_coordinates": "Vybrané GPS súradnice",
|
||||
"send_message": "Odoslať správu",
|
||||
"send_welcome_email": "Odoslať uvítací e-mail",
|
||||
"server_endpoint": "Koncový bod servera",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Zobraziť nasledujúci súbor",
|
||||
"view_previous_asset": "Zobraziť predchádzajúci súbor",
|
||||
"view_qr_code": "Zobraziť QR kód",
|
||||
"view_similar_photos": "Zobraziť podobné fotografie",
|
||||
"view_stack": "Zobraziť zoskupenie",
|
||||
"view_user": "Zobraziť používateľa",
|
||||
"viewer_remove_from_stack": "Odstrániť zo zoskupenia",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Odpri iskalne filtre",
|
||||
"options": "Možnosti",
|
||||
"or": "ali",
|
||||
"organize_into_albums": "Organiziraj v albume",
|
||||
"organize_into_albums_description": "Dodaj obstoječe fotografije v albume z uporabo trenutnih nastavitev sinhronizacije",
|
||||
"organize_your_library": "Organiziraj svojo knjižnico",
|
||||
"original": "izvirnik",
|
||||
"other": "drugo",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Status podpornika",
|
||||
"purchase_server_title": "Strežnik",
|
||||
"purchase_settings_server_activated": "Ključ izdelka strežnika upravlja skrbnik",
|
||||
"query_asset_id": "ID sredstva poizvedbe",
|
||||
"queue_status": "Čakalna vrsta {count}/{total}",
|
||||
"rating": "Ocena z zvezdicami",
|
||||
"rating_clear": "Počisti oceno",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Ogled naslednjega sredstva",
|
||||
"view_previous_asset": "Ogled prejšnjega sredstva",
|
||||
"view_qr_code": "Oglej si kodo QR",
|
||||
"view_similar_photos": "Oglejte si podobne fotografije",
|
||||
"view_stack": "Ogled sklada",
|
||||
"view_user": "Poglej uporabnika",
|
||||
"viewer_remove_from_stack": "Odstrani iz sklada",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Öppna sökfilter",
|
||||
"options": "Val",
|
||||
"or": "eller",
|
||||
"organize_into_albums": "Organisera i album",
|
||||
"organize_into_albums_description": "Lägg befintliga foton i album med aktuella synkroniseringsinställningar",
|
||||
"organize_your_library": "Organisera ditt bibliotek",
|
||||
"original": "original",
|
||||
"other": "Övrigt",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Supporterstatus",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Produktnyckeln för servern hanteras av administratören",
|
||||
"query_asset_id": "Fråga om objekts-ID",
|
||||
"queue_status": "Köande {count}/{total}",
|
||||
"rating": "Antal stjärnor",
|
||||
"rating_clear": "Ta bort betyg",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album",
|
||||
"selected": "Valda",
|
||||
"selected_count": "{count, plural, other {# valda}}",
|
||||
"selected_gps_coordinates": "valda GPS-koordinater",
|
||||
"selected_gps_coordinates": "Valda GPS-koordinater",
|
||||
"send_message": "Skicka meddelande",
|
||||
"send_welcome_email": "Skicka välkomstmejl",
|
||||
"server_endpoint": "Server-endpoint",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Visa nästa objekt",
|
||||
"view_previous_asset": "Visa föregående objekt",
|
||||
"view_qr_code": "Visa QR-kod",
|
||||
"view_similar_photos": "Visa liknande foton",
|
||||
"view_stack": "Visa Stapel",
|
||||
"view_user": "Visa Användare",
|
||||
"viewer_remove_from_stack": "Ta bort från Stapeln",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "Відкрийте фільтри пошуку",
|
||||
"options": "Налаштування",
|
||||
"or": "або",
|
||||
"organize_into_albums": "Упорядкувати в альбоми",
|
||||
"organize_into_albums_description": "Помістити наявні фотографії в альбоми, використовуючи поточні налаштування синхронізації",
|
||||
"organize_your_library": "Організуйте свою бібліотеку",
|
||||
"original": "оригінал",
|
||||
"other": "Інше",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "Статус підтримки",
|
||||
"purchase_server_title": "Сервер",
|
||||
"purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором",
|
||||
"query_asset_id": "Ідентифікатор ресурсу запиту",
|
||||
"queue_status": "У черзі {count} з {total}",
|
||||
"rating": "Зоряний рейтинг",
|
||||
"rating_clear": "Очистити рейтинг",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "Не вдалося створити альбом",
|
||||
"selected": "Обрано",
|
||||
"selected_count": "{count, plural, one {# обраний} other {# обраних}}",
|
||||
"selected_gps_coordinates": "вибрані GPS-координати",
|
||||
"selected_gps_coordinates": "Вибрані GPS-координати",
|
||||
"send_message": "Надіслати повідомлення",
|
||||
"send_welcome_email": "Надішліть вітальний лист",
|
||||
"server_endpoint": "Кінцева точка сервера",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "Переглянути наступний ресурс",
|
||||
"view_previous_asset": "Переглянути попередній ресурс",
|
||||
"view_qr_code": "Переглянути QR-код",
|
||||
"view_similar_photos": "Переглянути схожі фотографії",
|
||||
"view_stack": "Перегляд стеку",
|
||||
"view_user": "Переглянути користувача",
|
||||
"viewer_remove_from_stack": "Видалити зі стеку",
|
||||
|
||||
@@ -396,6 +396,8 @@
|
||||
"advanced_settings_prefer_remote_title": "偏好遠端影像",
|
||||
"advanced_settings_proxy_headers_subtitle": "定義 Immich 在每次網路請求時應該發送的代理標頭",
|
||||
"advanced_settings_proxy_headers_title": "代理標頭",
|
||||
"advanced_settings_readonly_mode_subtitle": "開啟唯讀模式後,照片只能瀏覽,像是多選影像、分享、投放、刪除等功能都會關閉。可在主畫面透過使用者頭像來開啟/關閉唯讀模式",
|
||||
"advanced_settings_readonly_mode_title": "唯讀模式",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "略過伺服器端點的 SSL 憑證驗證。自簽憑證時必須啟用此設定。",
|
||||
"advanced_settings_self_signed_ssl_title": "允許自簽的 SSL 憑證",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "當在網頁端執行刪除或還原操作時,自動在此裝置上刪除或還原該媒體",
|
||||
@@ -1073,6 +1075,10 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "此功能需要從 Google 載入外部資源才能正常運作。",
|
||||
"general": "一般",
|
||||
"geolocation_instruction_all_have_location": "此日期的所有項目已具有位置資料。請嘗試顯示所有項目或選擇其他日期",
|
||||
"geolocation_instruction_location": "點擊具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點",
|
||||
"geolocation_instruction_no_date": "選擇日期以管理該天的照片和影片位置資料",
|
||||
"geolocation_instruction_no_photos": "此日期沒有找到照片或影片。請選擇其他日期以顯示",
|
||||
"get_help": "線上求助",
|
||||
"get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連接至 Wi-Fi 網路",
|
||||
"getting_started": "開始使用",
|
||||
@@ -1262,6 +1268,7 @@
|
||||
"main_branch_warning": "您現在使用的是開發版本;我們強烈您建議使用正式發行版!",
|
||||
"main_menu": "主頁面",
|
||||
"make": "製造商",
|
||||
"manage_geolocation": "管理位置",
|
||||
"manage_shared_links": "管理共享連結",
|
||||
"manage_sharing_with_partners": "管理與親朋好友的分享",
|
||||
"manage_the_app_settings": "管理應用程式設定",
|
||||
@@ -1508,6 +1515,7 @@
|
||||
"profile_drawer_client_out_of_date_minor": "客戶端有小版本升級,請盡快升級至最新版。",
|
||||
"profile_drawer_client_server_up_to_date": "客戶端和服務端都是最新的",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_readonly_mode": "唯讀模式已開啟。請連點兩下使用者頭像圖示以退出。",
|
||||
"profile_drawer_server_out_of_date_major": "服務端有大版本升級,請盡快升級至最新版。",
|
||||
"profile_drawer_server_out_of_date_minor": "服務端有小版本升級,請盡快升級至最新版。",
|
||||
"profile_image_of_user": "{user} 的個人資料圖片",
|
||||
@@ -1553,6 +1561,8 @@
|
||||
"rating_description": "在資訊面板中顯示 EXIF 評等",
|
||||
"reaction_options": "反應選項",
|
||||
"read_changelog": "閱覽變更日誌",
|
||||
"readonly_mode_disabled": "唯讀模式已關閉",
|
||||
"readonly_mode_enabled": "唯讀模式已開啟",
|
||||
"reassign": "重新指定",
|
||||
"reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個檔案}}重新指定給{name, select, null {現有的人} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "已將 {count, plural, other {# 個檔案}}重新指定給一位新人物",
|
||||
@@ -1722,6 +1732,7 @@
|
||||
"select_user_for_sharing_page_err_album": "新增相簿失敗",
|
||||
"selected": "已選擇",
|
||||
"selected_count": "{count, plural, other {選了 # 項}}",
|
||||
"selected_gps_coordinates": "已選擇的 GPS 座標",
|
||||
"send_message": "傳訊息",
|
||||
"send_welcome_email": "傳送歡迎電子郵件",
|
||||
"server_endpoint": "伺服器端點",
|
||||
@@ -1832,8 +1843,10 @@
|
||||
"shift_to_permanent_delete": "按 ⇧ 永久刪除檔案",
|
||||
"show_album_options": "顯示相簿選項",
|
||||
"show_albums": "顯示相簿",
|
||||
"show_all_assets": "顯示所有項目",
|
||||
"show_all_people": "顯示所有人物",
|
||||
"show_and_hide_people": "顯示與隱藏人物",
|
||||
"show_assets_without_location": "顯示沒有地點的項目",
|
||||
"show_file_location": "顯示文件位置",
|
||||
"show_gallery": "顯示畫廊",
|
||||
"show_hidden_people": "顯示隱藏的人物",
|
||||
@@ -2015,6 +2028,7 @@
|
||||
"use_biometric": "使用生物辨識",
|
||||
"use_current_connection": "使用目前的連線",
|
||||
"use_custom_date_range": "改用自訂日期範圍",
|
||||
"use_this_location": "點擊以使用位置",
|
||||
"user": "使用者",
|
||||
"user_has_been_deleted": "此用戶已被刪除。",
|
||||
"user_id": "使用者 ID",
|
||||
|
||||
@@ -1417,6 +1417,8 @@
|
||||
"open_the_search_filters": "打开搜索过滤器",
|
||||
"options": "选项",
|
||||
"or": "或",
|
||||
"organize_into_albums": "整理成相册",
|
||||
"organize_into_albums_description": "使用当前同步设置将现有照片放入相册",
|
||||
"organize_your_library": "整理您的图库",
|
||||
"original": "原图",
|
||||
"other": "其它",
|
||||
@@ -1557,6 +1559,7 @@
|
||||
"purchase_server_description_2": "支持者状态",
|
||||
"purchase_server_title": "服务器",
|
||||
"purchase_settings_server_activated": "服务器产品密钥正在由管理员管理",
|
||||
"query_asset_id": "查询资产ID",
|
||||
"queue_status": "排队中 {count}/{total}",
|
||||
"rating": "星级",
|
||||
"rating_clear": "删除星级",
|
||||
@@ -1735,7 +1738,7 @@
|
||||
"select_user_for_sharing_page_err_album": "创建相册失败",
|
||||
"selected": "已选择",
|
||||
"selected_count": "{count, plural, other {#项已选择}}",
|
||||
"selected_gps_coordinates": "选定的GPS坐标",
|
||||
"selected_gps_coordinates": "已选定的GPS坐标",
|
||||
"send_message": "发送消息",
|
||||
"send_welcome_email": "发送欢迎邮件",
|
||||
"server_endpoint": "服务器 URL",
|
||||
@@ -2077,6 +2080,7 @@
|
||||
"view_next_asset": "查看下一项",
|
||||
"view_previous_asset": "查看上一项",
|
||||
"view_qr_code": "查看二维码",
|
||||
"view_similar_photos": "查看相似照片",
|
||||
"view_stack": "查看堆叠项目",
|
||||
"view_user": "查看用户",
|
||||
"viewer_remove_from_stack": "从堆叠中移除",
|
||||
|
||||
@@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:f3660c56d5b08d6c516360981bedc439f499b9bf37f46a216018da3777a74011 /uv /uvx /bin/
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.8.15@sha256:a5727064a0de127bdb7c9d3c1383f3a9ac307d9f2d8a391edc7896c54289ced0 /uv /uvx /bin/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
|
||||
29
mise.lock
Normal file
29
mise.lock
Normal file
@@ -0,0 +1,29 @@
|
||||
[tools.dart]
|
||||
version = "3.8.2"
|
||||
backend = "asdf:dart"
|
||||
|
||||
[tools.flutter]
|
||||
version = "3.32.8-stable"
|
||||
backend = "asdf:flutter"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.31.4"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64]
|
||||
checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955"
|
||||
size = 8821083
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip"
|
||||
|
||||
[tools.node]
|
||||
version = "22.19.0"
|
||||
backend = "core:node"
|
||||
|
||||
[tools.pnpm]
|
||||
version = "10.14.0"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[tools.pnpm.platforms.linux-x64]
|
||||
checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d"
|
||||
size = 66231525
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64"
|
||||
312
mise.toml
Normal file
312
mise.toml
Normal file
@@ -0,0 +1,312 @@
|
||||
[tools]
|
||||
node = "22.19.0"
|
||||
flutter = "3.32.8"
|
||||
pnpm = "10.14.0"
|
||||
dart = "3.8.2"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
version = "1.31.4"
|
||||
bin = "dcm"
|
||||
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
lockfile = true
|
||||
pin = true
|
||||
|
||||
# .github
|
||||
[tasks."github:install"]
|
||||
run = "pnpm install --filter github --frozen-lockfile"
|
||||
|
||||
[tasks."github:format"]
|
||||
env._.path = "./.github/node_modules/.bin"
|
||||
dir = ".github"
|
||||
run = "prettier --check ."
|
||||
|
||||
[tasks."github:format-fix"]
|
||||
env._.path = "./.github/node_modules/.bin"
|
||||
dir = ".github"
|
||||
run = "prettier --write ."
|
||||
|
||||
# @immich/cli
|
||||
[tasks."cli:install"]
|
||||
run = "pnpm install --filter @immich/cli --frozen-lockfile"
|
||||
|
||||
[tasks."cli:build"]
|
||||
env._.path = "./cli/node_modules/.bin"
|
||||
dir = "cli"
|
||||
run = "vite build"
|
||||
|
||||
[tasks."cli:test"]
|
||||
env._.path = "./cli/node_modules/.bin"
|
||||
dir = "cli"
|
||||
run = "vite"
|
||||
|
||||
[tasks."cli:lint"]
|
||||
env._.path = "./cli/node_modules/.bin"
|
||||
dir = "cli"
|
||||
run = "eslint \"src/**/*.ts\" --max-warnings 0"
|
||||
|
||||
[tasks."cli:lint-fix"]
|
||||
run = "mise run cli:lint --fix"
|
||||
|
||||
[tasks."cli:format"]
|
||||
env._.path = "./cli/node_modules/.bin"
|
||||
dir = "cli"
|
||||
run = "prettier --check ."
|
||||
|
||||
[tasks."cli:format-fix"]
|
||||
env._.path = "./cli/node_modules/.bin"
|
||||
dir = "cli"
|
||||
run = "prettier --write ."
|
||||
|
||||
[tasks."cli:check"]
|
||||
env._.path = "./cli/node_modules/.bin"
|
||||
dir = "cli"
|
||||
run = "tsc --noEmit"
|
||||
|
||||
# @immich/sdk
|
||||
[tasks."sdk:install"]
|
||||
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
|
||||
|
||||
[tasks."sdk:build"]
|
||||
env._.path = "./open-api/typescript-sdk/node_modules/.bin"
|
||||
dir = "./open-api/typescript-sdk"
|
||||
run = "tsc"
|
||||
|
||||
# docs
|
||||
[tasks."docs:install"]
|
||||
run = "pnpm install --filter documentation --frozen-lockfile"
|
||||
|
||||
[tasks."docs:start"]
|
||||
env._.path = "./docs/node_modules/.bin"
|
||||
dir = "docs"
|
||||
run = "docusaurus --port 3005"
|
||||
|
||||
[tasks."docs:build"]
|
||||
env._.path = "./docs/node_modules/.bin"
|
||||
dir = "docs"
|
||||
run = [
|
||||
"jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0",
|
||||
"docusaurus build",
|
||||
]
|
||||
|
||||
|
||||
[tasks."docs:preview"]
|
||||
env._.path = "./docs/node_modules/.bin"
|
||||
dir = "docs"
|
||||
run = "docusaurus serve"
|
||||
|
||||
|
||||
[tasks."docs:format"]
|
||||
env._.path = "./docs/node_modules/.bin"
|
||||
dir = "docs"
|
||||
run = "prettier --check ."
|
||||
|
||||
[tasks."docs:format-fix"]
|
||||
env._.path = "./docs/node_modules/.bin"
|
||||
dir = "docs"
|
||||
run = "prettier --write ."
|
||||
|
||||
|
||||
# e2e
|
||||
[tasks."e2e:install"]
|
||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||
|
||||
[tasks."e2e:test"]
|
||||
env._.path = "./e2e/node_modules/.bin"
|
||||
dir = "e2e"
|
||||
run = "vitest --run"
|
||||
|
||||
[tasks."e2e:test-web"]
|
||||
env._.path = "./e2e/node_modules/.bin"
|
||||
dir = "e2e"
|
||||
run = "playwright test"
|
||||
|
||||
[tasks."e2e:format"]
|
||||
env._.path = "./e2e/node_modules/.bin"
|
||||
dir = "e2e"
|
||||
run = "prettier --check ."
|
||||
|
||||
[tasks."e2e:format-fix"]
|
||||
env._.path = "./e2e/node_modules/.bin"
|
||||
dir = "e2e"
|
||||
run = "prettier --write ."
|
||||
|
||||
[tasks."e2e:lint"]
|
||||
env._.path = "./e2e/node_modules/.bin"
|
||||
dir = "e2e"
|
||||
run = "eslint \"src/**/*.ts\" --max-warnings 0"
|
||||
|
||||
[tasks."e2e:lint-fix"]
|
||||
run = "mise run e2e:lint --fix"
|
||||
|
||||
[tasks."e2e:check"]
|
||||
env._.path = "./e2e/node_modules/.bin"
|
||||
dir = "e2e"
|
||||
run = "tsc --noEmit"
|
||||
|
||||
# i18n
|
||||
[tasks."i18n:format"]
|
||||
run = "mise run i18n:format-fix"
|
||||
|
||||
[tasks."i18n:format-fix"]
|
||||
run = "pnpm dlx sort-json ./i18n/*.json"
|
||||
|
||||
|
||||
# server
|
||||
[tasks."server:install"]
|
||||
run = "pnpm install --filter immich --frozen-lockfile"
|
||||
|
||||
[tasks."server:build"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "nest build"
|
||||
|
||||
[tasks."server:test"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "vitest --config test/vitest.config.mjs"
|
||||
|
||||
[tasks."server:test-medium"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "vitest --config test/vitest.config.medium.mjs"
|
||||
|
||||
[tasks."server:format"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "prettier --check ."
|
||||
|
||||
[tasks."server:format-fix"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "prettier --write ."
|
||||
|
||||
[tasks."server:lint"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0"
|
||||
|
||||
[tasks."server:lint-fix"]
|
||||
run = "mise run server:lint --fix"
|
||||
|
||||
[tasks."server:check"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "tsc --noEmit"
|
||||
|
||||
[tasks."server:sql"]
|
||||
dir = "server"
|
||||
run = "node ./dist/bin/sync-open-api.js"
|
||||
|
||||
[tasks."server:open-api"]
|
||||
dir = "server"
|
||||
run = "node ./dist/bin/sync-open-api.js"
|
||||
|
||||
[tasks."server:migrations"]
|
||||
dir = "server"
|
||||
run = "node ./dist/bin/migrations.js"
|
||||
description = "Run database migration commands (create, generate, run, debug, or query)"
|
||||
|
||||
[tasks."server:schema-drop"]
|
||||
run = "mise run server:migrations query 'DROP schema public cascade; CREATE schema public;'"
|
||||
|
||||
[tasks."server:schema-reset"]
|
||||
run = "mise run server:schema-drop && mise run server:migrations run"
|
||||
|
||||
[tasks."server:email-dev"]
|
||||
env._.path = "./server/node_modules/.bin"
|
||||
dir = "server"
|
||||
run = "email dev -p 3050 --dir src/emails"
|
||||
|
||||
[tasks."server:checklist"]
|
||||
run = [
|
||||
"mise run server:install",
|
||||
"mise run server:format",
|
||||
"mise run server:lint",
|
||||
"mise run server:check",
|
||||
"mise run server:test-medium --run",
|
||||
"mise run server:test --run",
|
||||
]
|
||||
|
||||
|
||||
# web
|
||||
[tasks."web:install"]
|
||||
run = "pnpm install --filter immich-web --frozen-lockfile"
|
||||
|
||||
[tasks."web:svelte-kit-sync"]
|
||||
env._.path = "./web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "svelte-kit sync"
|
||||
|
||||
[tasks."web:build"]
|
||||
env._.path = "./web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "vite build"
|
||||
|
||||
[tasks."web:build-stats"]
|
||||
env.BUILD_STATS = "true"
|
||||
env._.path = "./web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "vite build"
|
||||
|
||||
[tasks."web:preview"]
|
||||
env._.path = "./web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "vite preview"
|
||||
|
||||
[tasks."web:start"]
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "vite dev --host 0.0.0.0 --port 3000"
|
||||
|
||||
[tasks."web:test"]
|
||||
depends = "web:svelte-kit-sync"
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "vitest"
|
||||
|
||||
[tasks."web:format"]
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "prettier --check ."
|
||||
|
||||
[tasks."web:format-fix"]
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "prettier --write ."
|
||||
|
||||
[tasks."web:lint"]
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "eslint . --max-warnings 0"
|
||||
|
||||
[tasks."web:lint-p"]
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "eslint-p . --max-warnings 0 --concurrency=4"
|
||||
|
||||
[tasks."web:lint-fix"]
|
||||
run = "mise run web:lint --fix"
|
||||
|
||||
[tasks."web:check"]
|
||||
depends = "web:svelte-kit-sync"
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "tsc --noEmit"
|
||||
|
||||
[tasks."web:check-svelte"]
|
||||
depends = "web:svelte-kit-sync"
|
||||
env._.path = "web/node_modules/.bin"
|
||||
dir = "web"
|
||||
run = "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore' --ignore src/lib/components/photos-page/asset-grid.svelte"
|
||||
|
||||
[tasks."web:checklist"]
|
||||
run = [
|
||||
"mise run web:install",
|
||||
"mise run web:format",
|
||||
"mise run web:check",
|
||||
"mise run web:test --run",
|
||||
"mise run web:lint",
|
||||
]
|
||||
@@ -61,9 +61,8 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface BackgroundWorkerFgHostApi {
|
||||
fun enableSyncWorker()
|
||||
fun enableUploadWorker()
|
||||
fun disableUploadWorker()
|
||||
fun enable()
|
||||
fun disable()
|
||||
|
||||
companion object {
|
||||
/** The codec used by BackgroundWorkerFgHostApi. */
|
||||
@@ -75,11 +74,11 @@ interface BackgroundWorkerFgHostApi {
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$separatedMessageChannelSuffix", codec)
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
api.enableSyncWorker()
|
||||
api.enable()
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||
@@ -91,27 +90,11 @@ interface BackgroundWorkerFgHostApi {
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec)
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
api.enableUploadWorker()
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
api.disableUploadWorker()
|
||||
api.disable()
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||
@@ -182,23 +165,6 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
|
||||
BackgroundWorkerPigeonCodec()
|
||||
}
|
||||
}
|
||||
fun onLocalSync(maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(maxSecondsArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
|
||||
@@ -16,11 +16,6 @@ import io.flutter.embedding.engine.loader.FlutterLoader
|
||||
|
||||
private const val TAG = "BackgroundWorker"
|
||||
|
||||
enum class BackgroundTaskType {
|
||||
LOCAL_SYNC,
|
||||
UPLOAD,
|
||||
}
|
||||
|
||||
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
||||
private val ctx: Context = context.applicationContext
|
||||
@@ -84,13 +79,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
* This method acts as a bridge between the native Android background task system and Flutter.
|
||||
*/
|
||||
override fun onInitialized() {
|
||||
val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0)
|
||||
val taskType = BackgroundTaskType.entries[taskTypeIndex]
|
||||
|
||||
when (taskType) {
|
||||
BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) }
|
||||
BackgroundTaskType.UPLOAD -> flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||
}
|
||||
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
@@ -141,8 +130,10 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||
* - Parameter success: Indicates whether the background task completed successfully
|
||||
*/
|
||||
private fun complete(success: Result) {
|
||||
Log.d(TAG, "About to complete BackupWorker with result: $success")
|
||||
isComplete = true
|
||||
engine?.destroy()
|
||||
engine = null
|
||||
flutterApi = null
|
||||
completionHandler.set(success)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package app.alextran.immich.background
|
||||
import android.content.Context
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
@@ -16,18 +14,13 @@ private const val TAG = "BackgroundUploadImpl"
|
||||
|
||||
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
private val ctx: Context = context.applicationContext
|
||||
override fun enableSyncWorker() {
|
||||
|
||||
override fun enable() {
|
||||
enqueueMediaObserver(ctx)
|
||||
Log.i(TAG, "Scheduled media observer")
|
||||
}
|
||||
|
||||
override fun enableUploadWorker() {
|
||||
updateUploadEnabled(ctx, true)
|
||||
Log.i(TAG, "Scheduled background upload tasks")
|
||||
}
|
||||
|
||||
override fun disableUploadWorker() {
|
||||
updateUploadEnabled(ctx, false)
|
||||
override fun disable() {
|
||||
WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME)
|
||||
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||
Log.i(TAG, "Cancelled background upload tasks")
|
||||
}
|
||||
@@ -36,25 +29,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||
|
||||
const val WORKER_DATA_TASK_TYPE = "taskType"
|
||||
|
||||
const val SHARED_PREF_NAME = "Immich::Background"
|
||||
const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled"
|
||||
|
||||
private fun updateUploadEnabled(context: Context, enabled: Boolean) {
|
||||
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
|
||||
putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun enqueueMediaObserver(ctx: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
||||
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
||||
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
||||
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
||||
.setTriggerContentUpdateDelay(5, TimeUnit.SECONDS)
|
||||
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
|
||||
.setTriggerContentUpdateDelay(30, TimeUnit.SECONDS)
|
||||
.setTriggerContentMaxDelay(3, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
||||
@@ -66,15 +48,13 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
||||
}
|
||||
|
||||
fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) {
|
||||
fun enqueueBackgroundWorker(ctx: Context) {
|
||||
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
||||
|
||||
val data = Data.Builder()
|
||||
data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal)
|
||||
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||
.setInputData(data.build()).build()
|
||||
.build()
|
||||
WorkManager.getInstance(ctx)
|
||||
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
||||
|
||||
|
||||
@@ -6,29 +6,17 @@ import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
private val ctx: Context = context.applicationContext
|
||||
private val ctx: Context = context.applicationContext
|
||||
|
||||
override fun doWork(): Result {
|
||||
Log.i("MediaObserver", "Content change detected, starting background worker")
|
||||
override fun doWork(): Result {
|
||||
Log.i("MediaObserver", "Content change detected, starting background worker")
|
||||
// Re-enqueue itself to listen for future changes
|
||||
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
||||
|
||||
// Enqueue backup worker only if there are new media changes
|
||||
if (triggeredContentUris.isNotEmpty()) {
|
||||
val type =
|
||||
if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC
|
||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type)
|
||||
}
|
||||
|
||||
// Re-enqueue itself to listen for future changes
|
||||
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun isBackupEnabled(context: Context): Boolean {
|
||||
val prefs =
|
||||
context.getSharedPreferences(
|
||||
BackgroundWorkerApiImpl.SHARED_PREF_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false)
|
||||
// Enqueue backup worker only if there are new media changes
|
||||
if (triggeredContentUris.isNotEmpty()) {
|
||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3011,
|
||||
"android.injected.version.name" => "1.140.1",
|
||||
"android.injected.version.code" => 3013,
|
||||
"android.injected.version.name" => "1.141.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
1
mobile/drift_schemas/main/drift_schema_v9.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v9.json
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -507,14 +507,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
@@ -543,14 +539,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
|
||||
@@ -73,9 +73,8 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol BackgroundWorkerFgHostApi {
|
||||
func enableSyncWorker() throws
|
||||
func enableUploadWorker() throws
|
||||
func disableUploadWorker() throws
|
||||
func enable() throws
|
||||
func disable() throws
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
@@ -84,44 +83,31 @@ class BackgroundWorkerFgHostApiSetup {
|
||||
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
let enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
enableSyncWorkerChannel.setMessageHandler { _, reply in
|
||||
enableChannel.setMessageHandler { _, reply in
|
||||
do {
|
||||
try api.enableSyncWorker()
|
||||
try api.enable()
|
||||
reply(wrapResult(nil))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enableSyncWorkerChannel.setMessageHandler(nil)
|
||||
enableChannel.setMessageHandler(nil)
|
||||
}
|
||||
let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
enableUploadWorkerChannel.setMessageHandler { _, reply in
|
||||
disableChannel.setMessageHandler { _, reply in
|
||||
do {
|
||||
try api.enableUploadWorker()
|
||||
try api.disable()
|
||||
reply(wrapResult(nil))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enableUploadWorkerChannel.setMessageHandler(nil)
|
||||
}
|
||||
let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
disableUploadWorkerChannel.setMessageHandler { _, reply in
|
||||
do {
|
||||
try api.disableUploadWorker()
|
||||
reply(wrapResult(nil))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disableUploadWorkerChannel.setMessageHandler(nil)
|
||||
disableChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +153,6 @@ class BackgroundWorkerBgHostApiSetup {
|
||||
}
|
||||
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||
protocol BackgroundWorkerFlutterApiProtocol {
|
||||
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||
@@ -182,24 +167,6 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
||||
var codec: BackgroundWorkerPigeonCodec {
|
||||
return BackgroundWorkerPigeonCodec.shared
|
||||
}
|
||||
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)"
|
||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||
channel.sendMessage([maxSecondsArg] as [Any?]) { response in
|
||||
guard let listResponse = response as? [Any?] else {
|
||||
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||
return
|
||||
}
|
||||
if listResponse.count > 1 {
|
||||
let code: String = listResponse[0] as! String
|
||||
let message: String? = nilOrValue(listResponse[1])
|
||||
let details: String? = nilOrValue(listResponse[2])
|
||||
completion(.failure(PigeonError(code: code, message: message, details: details)))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BackgroundTasks
|
||||
import Flutter
|
||||
|
||||
enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||
enum BackgroundTaskType { case refresh, processing }
|
||||
|
||||
/*
|
||||
* DEBUG: Testing Background Tasks in Xcode
|
||||
@@ -9,10 +9,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||
* To test background task functionality during development:
|
||||
* 1. Pause the application in Xcode debugger
|
||||
* 2. In the debugger console, enter one of the following commands:
|
||||
|
||||
## For local sync (short-running sync):
|
||||
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
||||
|
||||
## For background refresh (short-running sync):
|
||||
|
||||
@@ -24,8 +20,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||
|
||||
* To simulate task expiration (useful for testing expiration handlers):
|
||||
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
||||
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
||||
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
||||
@@ -120,19 +114,11 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
* This method acts as a bridge between the native iOS background task system and Flutter.
|
||||
*/
|
||||
func onInitialized() throws {
|
||||
switch self.taskType {
|
||||
case .refreshUpload, .processingUpload:
|
||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload,
|
||||
maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||
self.handleHostResult(result: result)
|
||||
})
|
||||
case .localSync:
|
||||
flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||
self.handleHostResult(result: result)
|
||||
})
|
||||
}
|
||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||
self.handleHostResult(result: result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancels the currently running background task, either due to timeout or external request.
|
||||
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
||||
@@ -154,6 +140,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
self.complete(success: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the result from Flutter API calls and determines the success/failure status.
|
||||
@@ -177,6 +164,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||
* - Parameter success: Indicates whether the background task completed successfully
|
||||
*/
|
||||
private func complete(success: Bool) {
|
||||
if(isComplete) {
|
||||
return
|
||||
}
|
||||
|
||||
isComplete = true
|
||||
engine.destroyContext()
|
||||
completionHandler(success)
|
||||
|
||||
@@ -1,77 +1,40 @@
|
||||
import BackgroundTasks
|
||||
|
||||
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
func enableSyncWorker() throws {
|
||||
BackgroundWorkerApiImpl.scheduleLocalSync()
|
||||
print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled")
|
||||
}
|
||||
|
||||
func enableUploadWorker() throws {
|
||||
BackgroundWorkerApiImpl.updateUploadEnabled(true)
|
||||
|
||||
BackgroundWorkerApiImpl.scheduleRefreshUpload()
|
||||
BackgroundWorkerApiImpl.scheduleProcessingUpload()
|
||||
print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks")
|
||||
}
|
||||
|
||||
func disableUploadWorker() throws {
|
||||
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
||||
BackgroundWorkerApiImpl.cancelUploadTasks()
|
||||
print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks")
|
||||
}
|
||||
|
||||
public static let backgroundUploadEnabledKey = "immich:background:backup:enabled"
|
||||
|
||||
private static let localSyncTaskID = "app.alextran.immich.background.localSync"
|
||||
private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload"
|
||||
private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload"
|
||||
|
||||
private static func updateUploadEnabled(_ isEnabled: Bool) {
|
||||
return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey)
|
||||
func enable() throws {
|
||||
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
||||
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
||||
print("BackgroundUploadImpl:enbale Background worker scheduled")
|
||||
}
|
||||
|
||||
private static func cancelUploadTasks() {
|
||||
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID);
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID);
|
||||
|
||||
func disable() throws {
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
||||
print("BackgroundUploadImpl:disableUploadWorker Disabled background workers")
|
||||
}
|
||||
|
||||
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
||||
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
||||
|
||||
public static func registerBackgroundWorkers() {
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in
|
||||
forTaskWithIdentifier: processingTaskID, using: nil) { task in
|
||||
if task is BGProcessingTask {
|
||||
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||
}
|
||||
}
|
||||
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in
|
||||
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
|
||||
if task is BGAppRefreshTask {
|
||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload)
|
||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: localSyncTaskID, using: nil) { task in
|
||||
if task is BGAppRefreshTask {
|
||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func scheduleLocalSync() {
|
||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID)
|
||||
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(backgroundRefresh)
|
||||
} catch {
|
||||
print("Could not schedule the local sync task \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func scheduleRefreshUpload() {
|
||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID)
|
||||
private static func scheduleRefreshWorker() {
|
||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID)
|
||||
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||
|
||||
do {
|
||||
@@ -81,8 +44,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
private static func scheduleProcessingUpload() {
|
||||
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID)
|
||||
private static func scheduleProcessingWorker() {
|
||||
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID)
|
||||
|
||||
backgroundProcessing.requiresNetworkConnectivity = true
|
||||
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
||||
@@ -94,29 +57,16 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) {
|
||||
let maxSeconds: Int?
|
||||
|
||||
switch taskType {
|
||||
case .localSync:
|
||||
maxSeconds = 15
|
||||
scheduleLocalSync()
|
||||
case .refreshUpload:
|
||||
maxSeconds = 20
|
||||
scheduleRefreshUpload()
|
||||
case .processingUpload:
|
||||
print("Unexpected background refresh task encountered")
|
||||
return;
|
||||
}
|
||||
|
||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
||||
scheduleRefreshWorker()
|
||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||
runBackgroundWorker(task: task, taskType: taskType, maxSeconds: maxSeconds)
|
||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
||||
}
|
||||
|
||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||
scheduleProcessingUpload()
|
||||
scheduleProcessingWorker()
|
||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||
runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil)
|
||||
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,23 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||
assetCache.countLimit = 10000
|
||||
return assetCache
|
||||
}()
|
||||
private static let activitySemaphore = DispatchSemaphore(value: 1)
|
||||
private static let willResignActiveObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.willResignActiveNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
processingQueue.suspend()
|
||||
activitySemaphore.wait()
|
||||
}
|
||||
private static let didBecomeActiveObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.didBecomeActiveNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
processingQueue.resume()
|
||||
activitySemaphore.signal()
|
||||
}
|
||||
|
||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
||||
Self.processingQueue.async {
|
||||
@@ -53,6 +70,7 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
||||
|
||||
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
||||
self.waitForActiveState()
|
||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
|
||||
}
|
||||
}
|
||||
@@ -142,6 +160,7 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||
return completion(Self.cancelledResult)
|
||||
}
|
||||
|
||||
self.waitForActiveState()
|
||||
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
|
||||
Self.removeRequest(requestId: requestId)
|
||||
}
|
||||
@@ -184,4 +203,9 @@ class ThumbnailApiImpl: ThumbnailApi {
|
||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
||||
return asset
|
||||
}
|
||||
|
||||
func waitForActiveState() {
|
||||
Self.activitySemaphore.wait()
|
||||
Self.activitySemaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,190 +1,189 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>app.alextran.immich.background.localSync</string>
|
||||
<string>app.alextran.immich.background.refreshUpload</string>
|
||||
<string>app.alextran.immich.background.processingUpload</string>
|
||||
<string>app.alextran.immich.backgroundFetch</string>
|
||||
<string>app.alextran.immich.backgroundProcessing</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ShareHandler</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.file-url</string>
|
||||
<string>public.image</string>
|
||||
<string>public.text</string>
|
||||
<string>public.movie</string>
|
||||
<string>public.url</string>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>ar</string>
|
||||
<string>ca</string>
|
||||
<string>cs</string>
|
||||
<string>da</string>
|
||||
<string>de</string>
|
||||
<string>es</string>
|
||||
<string>fi</string>
|
||||
<string>fr</string>
|
||||
<string>he</string>
|
||||
<string>hi</string>
|
||||
<string>hu</string>
|
||||
<string>it</string>
|
||||
<string>ja</string>
|
||||
<string>ko</string>
|
||||
<string>lv</string>
|
||||
<string>mn</string>
|
||||
<string>nb</string>
|
||||
<string>nl</string>
|
||||
<string>pl</string>
|
||||
<string>pt</string>
|
||||
<string>ro</string>
|
||||
<string>ru</string>
|
||||
<string>sk</string>
|
||||
<string>sl</string>
|
||||
<string>sr</string>
|
||||
<string>sv</string>
|
||||
<string>th</string>
|
||||
<string>uk</string>
|
||||
<string>vi</string>
|
||||
<string>zh</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>immich_mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.140.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Share Extension</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Deep Link</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>immich</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>219</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false />
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<string>No</string>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>We need local network permission to connect to the local server using IP address and
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>app.alextran.immich.background.refreshUpload</string>
|
||||
<string>app.alextran.immich.background.processingUpload</string>
|
||||
<string>app.alextran.immich.backgroundFetch</string>
|
||||
<string>app.alextran.immich.backgroundProcessing</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ShareHandler</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.file-url</string>
|
||||
<string>public.image</string>
|
||||
<string>public.text</string>
|
||||
<string>public.movie</string>
|
||||
<string>public.url</string>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>ar</string>
|
||||
<string>ca</string>
|
||||
<string>cs</string>
|
||||
<string>da</string>
|
||||
<string>de</string>
|
||||
<string>es</string>
|
||||
<string>fi</string>
|
||||
<string>fr</string>
|
||||
<string>he</string>
|
||||
<string>hi</string>
|
||||
<string>hu</string>
|
||||
<string>it</string>
|
||||
<string>ja</string>
|
||||
<string>ko</string>
|
||||
<string>lv</string>
|
||||
<string>mn</string>
|
||||
<string>nb</string>
|
||||
<string>nl</string>
|
||||
<string>pl</string>
|
||||
<string>pt</string>
|
||||
<string>ro</string>
|
||||
<string>ru</string>
|
||||
<string>sk</string>
|
||||
<string>sl</string>
|
||||
<string>sr</string>
|
||||
<string>sv</string>
|
||||
<string>th</string>
|
||||
<string>uk</string>
|
||||
<string>vi</string>
|
||||
<string>zh</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>immich_mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.140.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Share Extension</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Deep Link</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>immich</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>219</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<string>No</string>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>We need local network permission to connect to the local server using IP address and
|
||||
allow the casting feature to work</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true />
|
||||
</dict>
|
||||
</plist>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>We require this permission to access the local WiFi name</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -22,7 +22,7 @@ platform :ios do
|
||||
path: "./Runner.xcodeproj",
|
||||
)
|
||||
increment_version_number(
|
||||
version_number: "1.140.1"
|
||||
version_number: "1.141.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -15,6 +15,7 @@ class LocalAlbum {
|
||||
|
||||
final int assetCount;
|
||||
final BackupSelection backupSelection;
|
||||
final String? linkedRemoteAlbumId;
|
||||
|
||||
const LocalAlbum({
|
||||
required this.id,
|
||||
@@ -23,6 +24,7 @@ class LocalAlbum {
|
||||
this.assetCount = 0,
|
||||
this.backupSelection = BackupSelection.none,
|
||||
this.isIosSharedAlbum = false,
|
||||
this.linkedRemoteAlbumId,
|
||||
});
|
||||
|
||||
LocalAlbum copyWith({
|
||||
@@ -32,6 +34,7 @@ class LocalAlbum {
|
||||
int? assetCount,
|
||||
BackupSelection? backupSelection,
|
||||
bool? isIosSharedAlbum,
|
||||
String? linkedRemoteAlbumId,
|
||||
}) {
|
||||
return LocalAlbum(
|
||||
id: id ?? this.id,
|
||||
@@ -40,6 +43,7 @@ class LocalAlbum {
|
||||
assetCount: assetCount ?? this.assetCount,
|
||||
backupSelection: backupSelection ?? this.backupSelection,
|
||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +57,8 @@ class LocalAlbum {
|
||||
other.updatedAt == updatedAt &&
|
||||
other.assetCount == assetCount &&
|
||||
other.backupSelection == backupSelection &&
|
||||
other.isIosSharedAlbum == isIosSharedAlbum;
|
||||
other.isIosSharedAlbum == isIosSharedAlbum &&
|
||||
other.linkedRemoteAlbumId == linkedRemoteAlbumId;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -63,7 +68,8 @@ class LocalAlbum {
|
||||
updatedAt.hashCode ^
|
||||
assetCount.hashCode ^
|
||||
backupSelection.hashCode ^
|
||||
isIosSharedAlbum.hashCode;
|
||||
isIosSharedAlbum.hashCode ^
|
||||
linkedRemoteAlbumId.hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -75,6 +81,7 @@ updatedAt: $updatedAt,
|
||||
assetCount: $assetCount,
|
||||
backupSelection: $backupSelection,
|
||||
isIosSharedAlbum: $isIosSharedAlbum
|
||||
linkedRemoteAlbumId: $linkedRemoteAlbumId,
|
||||
}''';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
@@ -30,11 +31,9 @@ class BackgroundWorkerFgService {
|
||||
const BackgroundWorkerFgService(this._foregroundHostApi);
|
||||
|
||||
// TODO: Move this call to native side once old timeline is removed
|
||||
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
|
||||
Future<void> enable() => _foregroundHostApi.enable();
|
||||
|
||||
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker();
|
||||
|
||||
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
|
||||
Future<void> disable() => _foregroundHostApi.disable();
|
||||
}
|
||||
|
||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
@@ -43,7 +42,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
final Drift _drift;
|
||||
final DriftLogger _driftLogger;
|
||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||
final Logger _logger = Logger('BackgroundWorkerBgService');
|
||||
final Logger _logger = Logger('BackgroundUploadBgService');
|
||||
late final IsolateLockManager _lockManager;
|
||||
|
||||
bool _isCleanedUp = false;
|
||||
|
||||
@@ -59,6 +59,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
@@ -82,41 +83,31 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||
await FileDownloader().trackTasks();
|
||||
configureFileDownloaderNotifications();
|
||||
|
||||
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
|
||||
|
||||
// Notify the host that the background worker service has been initialized and is ready to use
|
||||
_backgroundHostApi.onInitialized();
|
||||
// Notify the host that the background upload service has been initialized and is ready to use
|
||||
debugPrint("Acquiring background worker lock");
|
||||
if (await _lockManager.acquireLock().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
_lockManager.cancel();
|
||||
return false;
|
||||
},
|
||||
)) {
|
||||
_logger.info("Acquired background worker lock");
|
||||
await _backgroundHostApi.onInitialized();
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.warning("Failed to acquire background worker lock");
|
||||
await _cleanup();
|
||||
await _backgroundHostApi.close();
|
||||
} catch (error, stack) {
|
||||
_logger.severe("Failed to initialize background worker", error, stack);
|
||||
_backgroundHostApi.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLocalSync(int? maxSeconds) async {
|
||||
try {
|
||||
_logger.info('Local background syncing started');
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
|
||||
await _syncAssets(hashTimeout: timeout, syncRemote: false);
|
||||
|
||||
sw.stop();
|
||||
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s");
|
||||
} catch (error, stack) {
|
||||
_logger.severe("Failed to complete local sync", error, stack);
|
||||
} finally {
|
||||
await _cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/* We do the following on Android upload
|
||||
* - Sync local assets
|
||||
* - Hash local assets 3 / 6 minutes
|
||||
* - Sync remote assets
|
||||
* - Check and requeue upload tasks
|
||||
*/
|
||||
@override
|
||||
Future<void> onAndroidUpload() async {
|
||||
try {
|
||||
@@ -135,14 +126,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
|
||||
/* We do the following on background upload
|
||||
* - Sync local assets
|
||||
* - Hash local assets
|
||||
* - Sync remote assets
|
||||
* - Check and requeue upload tasks
|
||||
*
|
||||
* The native side will not send the maxSeconds value for processing tasks
|
||||
*/
|
||||
@override
|
||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||
try {
|
||||
@@ -194,7 +177,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
await _drift.close();
|
||||
await _driftLogger.close();
|
||||
_ref.dispose();
|
||||
debugPrint("Background worker cleaned up");
|
||||
_lockManager.releaseLock();
|
||||
_logger.info("Background worker resources cleaned up");
|
||||
} catch (error, stack) {
|
||||
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
|
||||
}
|
||||
@@ -222,7 +206,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async {
|
||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||
@@ -244,10 +228,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
});
|
||||
|
||||
futures.add(localSyncFuture);
|
||||
if (syncRemote) {
|
||||
final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote();
|
||||
futures.add(remoteSyncFuture);
|
||||
}
|
||||
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
@@ -35,6 +36,7 @@ class HashService {
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
Future<void> hashAssets() async {
|
||||
_log.info("Starting hashing of assets");
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
// Sorted by backupSelection followed by isCloud
|
||||
final localAlbums = await _localAlbumRepository.getAll(
|
||||
@@ -49,7 +51,7 @@ class HashService {
|
||||
|
||||
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
||||
if (assetsToHash.isNotEmpty) {
|
||||
await _hashAssets(assetsToHash);
|
||||
await _hashAssets(album, assetsToHash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +62,7 @@ class HashService {
|
||||
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
||||
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
||||
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
||||
Future<void> _hashAssets(List<LocalAsset> assetsToHash) async {
|
||||
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
|
||||
int bytesProcessed = 0;
|
||||
final toHash = <_AssetToPath>[];
|
||||
|
||||
@@ -72,6 +74,9 @@ class HashService {
|
||||
|
||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||
if (file == null) {
|
||||
_log.warning(
|
||||
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt} from album: ${album.name}",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -79,17 +84,17 @@ class HashService {
|
||||
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
||||
|
||||
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
||||
await _processBatch(toHash);
|
||||
await _processBatch(album, toHash);
|
||||
toHash.clear();
|
||||
bytesProcessed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
await _processBatch(toHash);
|
||||
await _processBatch(album, toHash);
|
||||
}
|
||||
|
||||
/// Processes a batch of assets.
|
||||
Future<void> _processBatch(List<_AssetToPath> toHash) async {
|
||||
Future<void> _processBatch(LocalAlbum album, List<_AssetToPath> toHash) async {
|
||||
if (toHash.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@@ -114,7 +119,9 @@ class HashService {
|
||||
if (hash?.length == 20) {
|
||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||
} else {
|
||||
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
||||
_log.warning(
|
||||
"Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,16 @@ class LocalAlbumService {
|
||||
Future<int> getCount() {
|
||||
return _repository.getCount();
|
||||
}
|
||||
|
||||
Future<void> unlinkRemoteAlbum(String id) async {
|
||||
return _repository.unlinkRemoteAlbum(id);
|
||||
}
|
||||
|
||||
Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
|
||||
return _repository.linkRemoteAlbum(localAlbumId, remoteAlbumId);
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getBackupAlbums() {
|
||||
return _repository.getBackupAlbums();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ class RemoteAlbumService {
|
||||
return _repository.get(albumId);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||
return _repository.getByName(albumName, ownerId);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
RemoteAlbumSortMode sortMode, {
|
||||
@@ -80,7 +84,6 @@ class RemoteAlbumService {
|
||||
|
||||
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
|
||||
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
|
||||
|
||||
await _repository.create(album, assetIds);
|
||||
|
||||
return album;
|
||||
|
||||
101
mobile/lib/domain/services/sync_linked_album.service.dart
Normal file
101
mobile/lib/domain/services/sync_linked_album.service.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
|
||||
final syncLinkedAlbumServiceProvider = Provider(
|
||||
(ref) => SyncLinkedAlbumService(
|
||||
ref.watch(localAlbumRepository),
|
||||
ref.watch(remoteAlbumRepository),
|
||||
ref.watch(driftAlbumApiRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SyncLinkedAlbumService {
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
|
||||
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
|
||||
Future<void> syncLinkedAlbums(String userId) async {
|
||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
|
||||
await Future.wait(
|
||||
selectedAlbums.map((localAlbum) async {
|
||||
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
|
||||
if (linkedRemoteAlbumId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
|
||||
if (remoteAlbum == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get assets that are uploaded but not in the remote album
|
||||
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
|
||||
|
||||
if (assetIds.isNotEmpty) {
|
||||
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
|
||||
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a single local album to ensure proper linking with remote albums
|
||||
Future<void> _processLocalAlbum(LocalAlbum localAlbum, String ownerId) {
|
||||
final hasLinkedRemoteAlbum = localAlbum.linkedRemoteAlbumId != null;
|
||||
|
||||
if (hasLinkedRemoteAlbum) {
|
||||
return _handleLinkedAlbum(localAlbum);
|
||||
} else {
|
||||
return _handleUnlinkedAlbum(localAlbum, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles albums that are already linked to a remote album
|
||||
Future<void> _handleLinkedAlbum(LocalAlbum localAlbum) async {
|
||||
final remoteAlbumId = localAlbum.linkedRemoteAlbumId!;
|
||||
final remoteAlbum = await _remoteAlbumRepository.get(remoteAlbumId);
|
||||
|
||||
final remoteAlbumExists = remoteAlbum != null;
|
||||
if (!remoteAlbumExists) {
|
||||
return _localAlbumRepository.unlinkRemoteAlbum(localAlbum.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles albums that are not linked to any remote album
|
||||
Future<void> _handleUnlinkedAlbum(LocalAlbum localAlbum, String ownerId) async {
|
||||
final existingRemoteAlbum = await _remoteAlbumRepository.getByName(localAlbum.name, ownerId);
|
||||
|
||||
if (existingRemoteAlbum != null) {
|
||||
return _linkToExistingRemoteAlbum(localAlbum, existingRemoteAlbum);
|
||||
} else {
|
||||
return _createAndLinkNewRemoteAlbum(localAlbum);
|
||||
}
|
||||
}
|
||||
|
||||
/// Links a local album to an existing remote album
|
||||
Future<void> _linkToExistingRemoteAlbum(LocalAlbum localAlbum, dynamic existingRemoteAlbum) {
|
||||
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, existingRemoteAlbum.id);
|
||||
}
|
||||
|
||||
/// Creates a new remote album and links it to the local album
|
||||
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
|
||||
debugPrint("Creating new remote album for local album: ${localAlbum.name}");
|
||||
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
|
||||
await _remoteAlbumRepository.create(newRemoteAlbum, []);
|
||||
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
||||
import 'package:immich_mobile/utils/isolate.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
@@ -23,6 +24,7 @@ class BackgroundSyncManager {
|
||||
Cancelable<void>? _syncTask;
|
||||
Cancelable<void>? _syncWebsocketTask;
|
||||
Cancelable<void>? _deviceAlbumSyncTask;
|
||||
Cancelable<void>? _linkedAlbumSyncTask;
|
||||
Cancelable<void>? _hashTask;
|
||||
|
||||
BackgroundSyncManager({
|
||||
@@ -52,6 +54,12 @@ class BackgroundSyncManager {
|
||||
_syncWebsocketTask?.cancel();
|
||||
_syncWebsocketTask = null;
|
||||
|
||||
if (_linkedAlbumSyncTask != null) {
|
||||
futures.add(_linkedAlbumSyncTask!.future);
|
||||
}
|
||||
_linkedAlbumSyncTask?.cancel();
|
||||
_linkedAlbumSyncTask = null;
|
||||
|
||||
try {
|
||||
await Future.wait(futures);
|
||||
} on CanceledError {
|
||||
@@ -155,6 +163,17 @@ class BackgroundSyncManager {
|
||||
_syncWebsocketTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncLinkedAlbum() {
|
||||
if (_linkedAlbumSyncTask != null) {
|
||||
return _linkedAlbumSyncTask!.future;
|
||||
}
|
||||
|
||||
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated);
|
||||
return _linkedAlbumSyncTask!.whenComplete(() {
|
||||
_linkedAlbumSyncTask = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||
|
||||
235
mobile/lib/domain/utils/isolate_lock_manager.dart
Normal file
235
mobile/lib/domain/utils/isolate_lock_manager.dart
Normal file
@@ -0,0 +1,235 @@
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const String kIsolateLockManagerPort = "immich://isolate_mutex";
|
||||
|
||||
enum _LockStatus { active, released }
|
||||
|
||||
class _IsolateRequest {
|
||||
const _IsolateRequest();
|
||||
}
|
||||
|
||||
class _HeartbeatRequest extends _IsolateRequest {
|
||||
// Port for the receiver to send replies back
|
||||
final SendPort sendPort;
|
||||
|
||||
const _HeartbeatRequest(this.sendPort);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'type': 'heartbeat', 'sendPort': sendPort};
|
||||
}
|
||||
}
|
||||
|
||||
class _CloseRequest extends _IsolateRequest {
|
||||
const _CloseRequest();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'type': 'close'};
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateResponse {
|
||||
const _IsolateResponse();
|
||||
}
|
||||
|
||||
class _HeartbeatResponse extends _IsolateResponse {
|
||||
final _LockStatus status;
|
||||
|
||||
const _HeartbeatResponse(this.status);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'type': 'heartbeat', 'status': status.index};
|
||||
}
|
||||
}
|
||||
|
||||
typedef OnCloseLockHolderRequest = void Function();
|
||||
|
||||
class IsolateLockManager {
|
||||
final String _portName;
|
||||
bool _hasLock = false;
|
||||
ReceivePort? _receivePort;
|
||||
final OnCloseLockHolderRequest? _onCloseRequest;
|
||||
final Set<SendPort> _waitingIsolates = {};
|
||||
// Token object - a new one is created for each acquisition attempt
|
||||
Object? _currentAcquisitionToken;
|
||||
|
||||
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
|
||||
: _portName = portName ?? kIsolateLockManagerPort,
|
||||
_onCloseRequest = onCloseRequest;
|
||||
|
||||
Future<bool> acquireLock() async {
|
||||
if (_hasLock) {
|
||||
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a new token - this invalidates any previous attempt
|
||||
final token = _currentAcquisitionToken = Object();
|
||||
|
||||
final ReceivePort rp = _receivePort = ReceivePort(_portName);
|
||||
final SendPort sp = rp.sendPort;
|
||||
|
||||
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
|
||||
// This attempt was superseded by a newer one in the same isolate
|
||||
if (_currentAcquisitionToken != token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _lockReleasedByHolder(token);
|
||||
}
|
||||
|
||||
_hasLock = true;
|
||||
rp.listen(_onRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _lockReleasedByHolder(Object token) async {
|
||||
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
|
||||
debugPrint("Found lock holder: $holder");
|
||||
if (holder == null) {
|
||||
// No holder, try and acquire lock
|
||||
return;
|
||||
}
|
||||
|
||||
final ReceivePort tempRp = ReceivePort();
|
||||
final SendPort tempSp = tempRp.sendPort;
|
||||
final bs = tempRp.asBroadcastStream();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
// Send a heartbeat request with the send port to receive reply from the holder
|
||||
|
||||
debugPrint("Sending heartbeat request to lock holder");
|
||||
holder.send(_HeartbeatRequest(tempSp).toJson());
|
||||
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
|
||||
|
||||
debugPrint("Received heartbeat response from lock holder: $answer");
|
||||
// This attempt was superseded by a newer one in the same isolate
|
||||
if (_currentAcquisitionToken != token) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (answer == null) {
|
||||
// Holder failed, most likely killed without calling releaseLock
|
||||
// Check if a different waiting isolate took the lock
|
||||
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
|
||||
// No, remove the stale lock
|
||||
IsolateNameServer.removePortNameMapping(_portName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Unknown message type received for heartbeat request. Try again
|
||||
_IsolateResponse? response = _parseResponse(answer);
|
||||
if (response == null || response is! _HeartbeatResponse) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.status == _LockStatus.released) {
|
||||
// Holder has released the lock
|
||||
break;
|
||||
}
|
||||
|
||||
// If the _LockStatus is active, we check again if the task completed
|
||||
// by sending a released messaged again, if not, send a new heartbeat again
|
||||
|
||||
// Check if the holder completed its task after the heartbeat
|
||||
answer = await bs.first.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
|
||||
);
|
||||
|
||||
response = _parseResponse(answer);
|
||||
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Timeout or error
|
||||
} finally {
|
||||
tempRp.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_IsolateRequest? _parseRequest(dynamic msg) {
|
||||
if (msg is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (msg['type']) {
|
||||
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
|
||||
'close' => const _CloseRequest(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
_IsolateResponse? _parseResponse(dynamic msg) {
|
||||
if (msg is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (msg['type']) {
|
||||
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Executed in the isolate with the lock
|
||||
void _onRequest(dynamic msg) {
|
||||
final request = _parseRequest(msg);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request is _HeartbeatRequest) {
|
||||
// Add the send port to the list of waiting isolates
|
||||
_waitingIsolates.add(request.sendPort);
|
||||
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
if (request is _CloseRequest) {
|
||||
_onCloseRequest?.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void releaseLock() {
|
||||
if (_hasLock) {
|
||||
IsolateNameServer.removePortNameMapping(_portName);
|
||||
|
||||
// Notify waiting isolates
|
||||
for (final port in _waitingIsolates) {
|
||||
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
|
||||
}
|
||||
_waitingIsolates.clear();
|
||||
|
||||
_hasLock = false;
|
||||
}
|
||||
|
||||
_receivePort?.close();
|
||||
_receivePort = null;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (_hasLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Cancelling ongoing acquire lock attempts");
|
||||
// Create a new token to invalidate ongoing acquire lock attempts
|
||||
_currentAcquisitionToken = Object();
|
||||
}
|
||||
|
||||
void requestHolderToClose() {
|
||||
if (_hasLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
|
||||
}
|
||||
}
|
||||
11
mobile/lib/domain/utils/sync_linked_album.dart
Normal file
11
mobile/lib/domain/utils/sync_linked_album.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
|
||||
final user = ref.read(currentUserProvider);
|
||||
if (user == null) {
|
||||
return Future.value();
|
||||
}
|
||||
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
@@ -11,9 +13,26 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
||||
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
|
||||
|
||||
// // Linked album for putting assets to the remote album after finished uploading
|
||||
TextColumn get linkedRemoteAlbumId =>
|
||||
text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.setNull).nullable()();
|
||||
|
||||
// Used for mark & sweep
|
||||
BoolColumn get marker_ => boolean().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
||||
LocalAlbum toDto({int assetCount = 0}) {
|
||||
return LocalAlbum(
|
||||
id: id,
|
||||
name: name,
|
||||
updatedAt: updatedAt,
|
||||
assetCount: assetCount,
|
||||
backupSelection: backupSelection,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart' as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'
|
||||
as i3;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:drift/internal/modular.dart' as i6;
|
||||
|
||||
typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
|
||||
i1.LocalAlbumEntityCompanion Function({
|
||||
@@ -15,6 +18,7 @@ typedef $$LocalAlbumEntityTableCreateCompanionBuilder =
|
||||
i0.Value<DateTime> updatedAt,
|
||||
required i2.BackupSelection backupSelection,
|
||||
i0.Value<bool> isIosSharedAlbum,
|
||||
i0.Value<String?> linkedRemoteAlbumId,
|
||||
i0.Value<bool?> marker_,
|
||||
});
|
||||
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
|
||||
@@ -24,9 +28,57 @@ typedef $$LocalAlbumEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<DateTime> updatedAt,
|
||||
i0.Value<i2.BackupSelection> backupSelection,
|
||||
i0.Value<bool> isIosSharedAlbum,
|
||||
i0.Value<String?> linkedRemoteAlbumId,
|
||||
i0.Value<bool?> marker_,
|
||||
});
|
||||
|
||||
final class $$LocalAlbumEntityTableReferences
|
||||
extends
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$LocalAlbumEntityTable,
|
||||
i1.LocalAlbumEntityData
|
||||
> {
|
||||
$$LocalAlbumEntityTableReferences(
|
||||
super.$_db,
|
||||
super.$_table,
|
||||
super.$_typedResult,
|
||||
);
|
||||
|
||||
static i5.$RemoteAlbumEntityTable _linkedRemoteAlbumIdTable(
|
||||
i0.GeneratedDatabase db,
|
||||
) => i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity')
|
||||
.createAlias(
|
||||
i0.$_aliasNameGenerator(
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$LocalAlbumEntityTable>('local_album_entity')
|
||||
.linkedRemoteAlbumId,
|
||||
i6.ReadDatabaseContainer(
|
||||
db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity').id,
|
||||
),
|
||||
);
|
||||
|
||||
i5.$$RemoteAlbumEntityTableProcessedTableManager? get linkedRemoteAlbumId {
|
||||
final $_column = $_itemColumn<String>('linked_remote_album_id');
|
||||
if ($_column == null) return null;
|
||||
final manager = i5
|
||||
.$$RemoteAlbumEntityTableTableManager(
|
||||
$_db,
|
||||
i6.ReadDatabaseContainer(
|
||||
$_db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
)
|
||||
.filter((f) => f.id.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_linkedRemoteAlbumIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
|
||||
$$LocalAlbumEntityTableFilterComposer({
|
||||
@@ -66,6 +118,33 @@ class $$LocalAlbumEntityTableFilterComposer
|
||||
column: $table.marker_,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i5.$$RemoteAlbumEntityTableFilterComposer get linkedRemoteAlbumId {
|
||||
final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAlbumEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableOrderingComposer
|
||||
@@ -106,6 +185,34 @@ class $$LocalAlbumEntityTableOrderingComposer
|
||||
column: $table.marker_,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i5.$$RemoteAlbumEntityTableOrderingComposer get linkedRemoteAlbumId {
|
||||
final i5.$$RemoteAlbumEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAlbumEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableAnnotationComposer
|
||||
@@ -139,6 +246,34 @@ class $$LocalAlbumEntityTableAnnotationComposer
|
||||
|
||||
i0.GeneratedColumn<bool> get marker_ =>
|
||||
$composableBuilder(column: $table.marker_, builder: (column) => column);
|
||||
|
||||
i5.$$RemoteAlbumEntityTableAnnotationComposer get linkedRemoteAlbumId {
|
||||
final i5.$$RemoteAlbumEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.linkedRemoteAlbumId,
|
||||
referencedTable: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder:
|
||||
(
|
||||
joinBuilder, {
|
||||
$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
}) => i5.$$RemoteAlbumEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer(
|
||||
$db,
|
||||
).resultSet<i5.$RemoteAlbumEntityTable>('remote_album_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
),
|
||||
);
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableTableManager
|
||||
@@ -152,16 +287,9 @@ class $$LocalAlbumEntityTableTableManager
|
||||
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
||||
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
||||
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$LocalAlbumEntityTable,
|
||||
i1.LocalAlbumEntityData
|
||||
>,
|
||||
),
|
||||
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
i0.PrefetchHooks Function({bool linkedRemoteAlbumId})
|
||||
> {
|
||||
$$LocalAlbumEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
@@ -187,6 +315,7 @@ class $$LocalAlbumEntityTableTableManager
|
||||
i0.Value<i2.BackupSelection> backupSelection =
|
||||
const i0.Value.absent(),
|
||||
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
|
||||
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
|
||||
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||
}) => i1.LocalAlbumEntityCompanion(
|
||||
id: id,
|
||||
@@ -194,6 +323,7 @@ class $$LocalAlbumEntityTableTableManager
|
||||
updatedAt: updatedAt,
|
||||
backupSelection: backupSelection,
|
||||
isIosSharedAlbum: isIosSharedAlbum,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||
marker_: marker_,
|
||||
),
|
||||
createCompanionCallback:
|
||||
@@ -203,6 +333,7 @@ class $$LocalAlbumEntityTableTableManager
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
required i2.BackupSelection backupSelection,
|
||||
i0.Value<bool> isIosSharedAlbum = const i0.Value.absent(),
|
||||
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
|
||||
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||
}) => i1.LocalAlbumEntityCompanion.insert(
|
||||
id: id,
|
||||
@@ -210,12 +341,60 @@ class $$LocalAlbumEntityTableTableManager
|
||||
updatedAt: updatedAt,
|
||||
backupSelection: backupSelection,
|
||||
isIosSharedAlbum: isIosSharedAlbum,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||
marker_: marker_,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
.map(
|
||||
(e) => (
|
||||
e.readTable(table),
|
||||
i1.$$LocalAlbumEntityTableReferences(db, table, e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
prefetchHooksCallback: ({linkedRemoteAlbumId = false}) {
|
||||
return i0.PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
addJoins:
|
||||
<
|
||||
T extends i0.TableManagerState<
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic
|
||||
>
|
||||
>(state) {
|
||||
if (linkedRemoteAlbumId) {
|
||||
state =
|
||||
state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.linkedRemoteAlbumId,
|
||||
referencedTable: i1
|
||||
.$$LocalAlbumEntityTableReferences
|
||||
._linkedRemoteAlbumIdTable(db),
|
||||
referencedColumn: i1
|
||||
.$$LocalAlbumEntityTableReferences
|
||||
._linkedRemoteAlbumIdTable(db)
|
||||
.id,
|
||||
)
|
||||
as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -230,16 +409,9 @@ typedef $$LocalAlbumEntityTableProcessedTableManager =
|
||||
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
||||
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
||||
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$LocalAlbumEntityTable,
|
||||
i1.LocalAlbumEntityData
|
||||
>,
|
||||
),
|
||||
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
i0.PrefetchHooks Function({bool linkedRemoteAlbumId})
|
||||
>;
|
||||
|
||||
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
@@ -308,6 +480,20 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
),
|
||||
defaultValue: const i4.Constant(false),
|
||||
);
|
||||
static const i0.VerificationMeta _linkedRemoteAlbumIdMeta =
|
||||
const i0.VerificationMeta('linkedRemoteAlbumId');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> linkedRemoteAlbumId =
|
||||
i0.GeneratedColumn<String>(
|
||||
'linked_remote_album_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
|
||||
),
|
||||
);
|
||||
static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta(
|
||||
'marker_',
|
||||
);
|
||||
@@ -329,6 +515,7 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
updatedAt,
|
||||
backupSelection,
|
||||
isIosSharedAlbum,
|
||||
linkedRemoteAlbumId,
|
||||
marker_,
|
||||
];
|
||||
@override
|
||||
@@ -371,6 +558,15 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('linked_remote_album_id')) {
|
||||
context.handle(
|
||||
_linkedRemoteAlbumIdMeta,
|
||||
linkedRemoteAlbumId.isAcceptableOrUnknown(
|
||||
data['linked_remote_album_id']!,
|
||||
_linkedRemoteAlbumIdMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('marker')) {
|
||||
context.handle(
|
||||
_marker_Meta,
|
||||
@@ -412,6 +608,10 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_ios_shared_album'],
|
||||
)!,
|
||||
linkedRemoteAlbumId: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}linked_remote_album_id'],
|
||||
),
|
||||
marker_: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.bool,
|
||||
data['${effectivePrefix}marker'],
|
||||
@@ -441,6 +641,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
final DateTime updatedAt;
|
||||
final i2.BackupSelection backupSelection;
|
||||
final bool isIosSharedAlbum;
|
||||
final String? linkedRemoteAlbumId;
|
||||
final bool? marker_;
|
||||
const LocalAlbumEntityData({
|
||||
required this.id,
|
||||
@@ -448,6 +649,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
required this.updatedAt,
|
||||
required this.backupSelection,
|
||||
required this.isIosSharedAlbum,
|
||||
this.linkedRemoteAlbumId,
|
||||
this.marker_,
|
||||
});
|
||||
@override
|
||||
@@ -464,6 +666,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
);
|
||||
}
|
||||
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum);
|
||||
if (!nullToAbsent || linkedRemoteAlbumId != null) {
|
||||
map['linked_remote_album_id'] = i0.Variable<String>(linkedRemoteAlbumId);
|
||||
}
|
||||
if (!nullToAbsent || marker_ != null) {
|
||||
map['marker'] = i0.Variable<bool>(marker_);
|
||||
}
|
||||
@@ -482,6 +687,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
|
||||
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
|
||||
isIosSharedAlbum: serializer.fromJson<bool>(json['isIosSharedAlbum']),
|
||||
linkedRemoteAlbumId: serializer.fromJson<String?>(
|
||||
json['linkedRemoteAlbumId'],
|
||||
),
|
||||
marker_: serializer.fromJson<bool?>(json['marker_']),
|
||||
);
|
||||
}
|
||||
@@ -498,6 +706,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
),
|
||||
),
|
||||
'isIosSharedAlbum': serializer.toJson<bool>(isIosSharedAlbum),
|
||||
'linkedRemoteAlbumId': serializer.toJson<String?>(linkedRemoteAlbumId),
|
||||
'marker_': serializer.toJson<bool?>(marker_),
|
||||
};
|
||||
}
|
||||
@@ -508,6 +717,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
DateTime? updatedAt,
|
||||
i2.BackupSelection? backupSelection,
|
||||
bool? isIosSharedAlbum,
|
||||
i0.Value<String?> linkedRemoteAlbumId = const i0.Value.absent(),
|
||||
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||
}) => i1.LocalAlbumEntityData(
|
||||
id: id ?? this.id,
|
||||
@@ -515,6 +725,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
backupSelection: backupSelection ?? this.backupSelection,
|
||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId.present
|
||||
? linkedRemoteAlbumId.value
|
||||
: this.linkedRemoteAlbumId,
|
||||
marker_: marker_.present ? marker_.value : this.marker_,
|
||||
);
|
||||
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
|
||||
@@ -528,6 +741,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
isIosSharedAlbum: data.isIosSharedAlbum.present
|
||||
? data.isIosSharedAlbum.value
|
||||
: this.isIosSharedAlbum,
|
||||
linkedRemoteAlbumId: data.linkedRemoteAlbumId.present
|
||||
? data.linkedRemoteAlbumId.value
|
||||
: this.linkedRemoteAlbumId,
|
||||
marker_: data.marker_.present ? data.marker_.value : this.marker_,
|
||||
);
|
||||
}
|
||||
@@ -540,6 +756,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('backupSelection: $backupSelection, ')
|
||||
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
|
||||
..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ')
|
||||
..write('marker_: $marker_')
|
||||
..write(')'))
|
||||
.toString();
|
||||
@@ -552,6 +769,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
updatedAt,
|
||||
backupSelection,
|
||||
isIosSharedAlbum,
|
||||
linkedRemoteAlbumId,
|
||||
marker_,
|
||||
);
|
||||
@override
|
||||
@@ -563,6 +781,7 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.backupSelection == this.backupSelection &&
|
||||
other.isIosSharedAlbum == this.isIosSharedAlbum &&
|
||||
other.linkedRemoteAlbumId == this.linkedRemoteAlbumId &&
|
||||
other.marker_ == this.marker_);
|
||||
}
|
||||
|
||||
@@ -573,6 +792,7 @@ class LocalAlbumEntityCompanion
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
final i0.Value<i2.BackupSelection> backupSelection;
|
||||
final i0.Value<bool> isIosSharedAlbum;
|
||||
final i0.Value<String?> linkedRemoteAlbumId;
|
||||
final i0.Value<bool?> marker_;
|
||||
const LocalAlbumEntityCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
@@ -580,6 +800,7 @@ class LocalAlbumEntityCompanion
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
this.backupSelection = const i0.Value.absent(),
|
||||
this.isIosSharedAlbum = const i0.Value.absent(),
|
||||
this.linkedRemoteAlbumId = const i0.Value.absent(),
|
||||
this.marker_ = const i0.Value.absent(),
|
||||
});
|
||||
LocalAlbumEntityCompanion.insert({
|
||||
@@ -588,6 +809,7 @@ class LocalAlbumEntityCompanion
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
required i2.BackupSelection backupSelection,
|
||||
this.isIosSharedAlbum = const i0.Value.absent(),
|
||||
this.linkedRemoteAlbumId = const i0.Value.absent(),
|
||||
this.marker_ = const i0.Value.absent(),
|
||||
}) : id = i0.Value(id),
|
||||
name = i0.Value(name),
|
||||
@@ -598,6 +820,7 @@ class LocalAlbumEntityCompanion
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
i0.Expression<int>? backupSelection,
|
||||
i0.Expression<bool>? isIosSharedAlbum,
|
||||
i0.Expression<String>? linkedRemoteAlbumId,
|
||||
i0.Expression<bool>? marker_,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
@@ -606,6 +829,8 @@ class LocalAlbumEntityCompanion
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (backupSelection != null) 'backup_selection': backupSelection,
|
||||
if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum,
|
||||
if (linkedRemoteAlbumId != null)
|
||||
'linked_remote_album_id': linkedRemoteAlbumId,
|
||||
if (marker_ != null) 'marker': marker_,
|
||||
});
|
||||
}
|
||||
@@ -616,6 +841,7 @@ class LocalAlbumEntityCompanion
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
i0.Value<i2.BackupSelection>? backupSelection,
|
||||
i0.Value<bool>? isIosSharedAlbum,
|
||||
i0.Value<String?>? linkedRemoteAlbumId,
|
||||
i0.Value<bool?>? marker_,
|
||||
}) {
|
||||
return i1.LocalAlbumEntityCompanion(
|
||||
@@ -624,6 +850,7 @@ class LocalAlbumEntityCompanion
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
backupSelection: backupSelection ?? this.backupSelection,
|
||||
isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId,
|
||||
marker_: marker_ ?? this.marker_,
|
||||
);
|
||||
}
|
||||
@@ -650,6 +877,11 @@ class LocalAlbumEntityCompanion
|
||||
if (isIosSharedAlbum.present) {
|
||||
map['is_ios_shared_album'] = i0.Variable<bool>(isIosSharedAlbum.value);
|
||||
}
|
||||
if (linkedRemoteAlbumId.present) {
|
||||
map['linked_remote_album_id'] = i0.Variable<String>(
|
||||
linkedRemoteAlbumId.value,
|
||||
);
|
||||
}
|
||||
if (marker_.present) {
|
||||
map['marker'] = i0.Variable<bool>(marker_.value);
|
||||
}
|
||||
@@ -664,6 +896,7 @@ class LocalAlbumEntityCompanion
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('backupSelection: $backupSelection, ')
|
||||
..write('isIosSharedAlbum: $isIosSharedAlbum, ')
|
||||
..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ')
|
||||
..write('marker_: $marker_')
|
||||
..write(')'))
|
||||
.toString();
|
||||
|
||||
@@ -20,7 +20,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
||||
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
LocalAsset toDto() => LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
|
||||
@@ -4,9 +4,10 @@ import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import "package:immich_mobile/utils/database.utils.dart";
|
||||
|
||||
final backupRepositoryProvider = Provider<DriftBackupRepository>(
|
||||
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
|
||||
|
||||
@@ -68,7 +68,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||
|
||||
@override
|
||||
int get schemaVersion => 8;
|
||||
int get schemaVersion => 9;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -123,6 +123,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
from7To8: (m, v8) async {
|
||||
await m.create(v8.storeEntity);
|
||||
},
|
||||
from8To9: (m, v9) async {
|
||||
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i9;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i9;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i10;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
as i11;
|
||||
@@ -48,19 +48,19 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this);
|
||||
late final i4.$LocalAssetEntityTable localAssetEntity = i4
|
||||
.$LocalAssetEntityTable(this);
|
||||
late final i5.$LocalAlbumEntityTable localAlbumEntity = i5
|
||||
late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5
|
||||
.$RemoteAlbumEntityTable(this);
|
||||
late final i6.$LocalAlbumEntityTable localAlbumEntity = i6
|
||||
.$LocalAlbumEntityTable(this);
|
||||
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i6
|
||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
|
||||
.$LocalAlbumAssetEntityTable(this);
|
||||
late final i7.$UserMetadataEntityTable userMetadataEntity = i7
|
||||
late final i8.$UserMetadataEntityTable userMetadataEntity = i8
|
||||
.$UserMetadataEntityTable(this);
|
||||
late final i8.$PartnerEntityTable partnerEntity = i8.$PartnerEntityTable(
|
||||
late final i9.$PartnerEntityTable partnerEntity = i9.$PartnerEntityTable(
|
||||
this,
|
||||
);
|
||||
late final i9.$RemoteExifEntityTable remoteExifEntity = i9
|
||||
late final i10.$RemoteExifEntityTable remoteExifEntity = i10
|
||||
.$RemoteExifEntityTable(this);
|
||||
late final i10.$RemoteAlbumEntityTable remoteAlbumEntity = i10
|
||||
.$RemoteAlbumEntityTable(this);
|
||||
late final i11.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i11
|
||||
.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i12.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i12
|
||||
@@ -84,6 +84,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
i4.idxLocalAssetChecksum,
|
||||
@@ -94,7 +95,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
memoryEntity,
|
||||
@@ -102,7 +102,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
i9.idxLatLng,
|
||||
i10.idxLatLng,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
@@ -123,6 +123,33 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
),
|
||||
result: [i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_album_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('local_album_entity', kind: i0.UpdateKind.update),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'local_asset_entity',
|
||||
@@ -173,24 +200,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'user_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'remote_asset_entity',
|
||||
@@ -290,18 +299,18 @@ class $DriftManager {
|
||||
i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
|
||||
i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i7.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i7.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i8.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i8.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i9.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i9.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i10.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i10.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i8.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i8.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i9.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i9.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i10.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i10.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i11.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i11.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
_db,
|
||||
|
||||
@@ -3435,6 +3435,391 @@ i1.GeneratedColumn<int> _column_89(String aliasedName) =>
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
|
||||
final class Schema9 extends i0.VersionedSchema {
|
||||
Schema9({required super.database}) : super(version: 9);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAssetChecksum,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
idxLatLng,
|
||||
];
|
||||
late final Shape16 userEntity = Shape16(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_5,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape17 remoteAssetEntity = Shape17(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_86,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 stackEntity = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape2 localAssetEntity = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape9 remoteAlbumEntity = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_56,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape19 localAlbumEntity = Shape19(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_5,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_90,
|
||||
_column_33,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 localAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_34, _column_35],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||
'idx_remote_asset_owner_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_25, _column_26, _column_27],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape5 partnerEntity = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_28, _column_29, _column_30],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape8 remoteExifEntity = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_11,
|
||||
_column_10,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_55,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_36, _column_60],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_60, _column_25, _column_61],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 memoryEntity = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_18,
|
||||
_column_15,
|
||||
_column_8,
|
||||
_column_62,
|
||||
_column_63,
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_67,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_36, _column_68],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape14 personEntity = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_1,
|
||||
_column_69,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_73,
|
||||
_column_74,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape15 assetFaceEntity = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_36,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_87, _column_88, _column_89],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape19 extends i0.VersionedTable {
|
||||
Shape19({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get backupSelection =>
|
||||
columnsByName['backup_selection']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<bool> get isIosSharedAlbum =>
|
||||
columnsByName['is_ios_shared_album']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get linkedRemoteAlbumId =>
|
||||
columnsByName['linked_remote_album_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get marker_ =>
|
||||
columnsByName['marker']! as i1.GeneratedColumn<bool>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_90(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'linked_remote_album_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES remote_album_entity (id) ON DELETE SET NULL',
|
||||
),
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -3443,6 +3828,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -3481,6 +3867,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from7To8(migrator, schema);
|
||||
return 8;
|
||||
case 8:
|
||||
final schema = Schema9(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from8To9(migrator, schema);
|
||||
return 9;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -3495,6 +3886,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -3504,5 +3896,6 @@ i1.OnUpgrade stepByStep({
|
||||
from5To6: from5To6,
|
||||
from6To7: from6To7,
|
||||
from7To8: from7To8,
|
||||
from8To9: from8To9,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/database.utils.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
||||
@@ -49,6 +50,13 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getBackupAlbums() async {
|
||||
final query = _db.localAlbumEntity.select()
|
||||
..where((row) => row.backupSelection.equalsValue(BackupSelection.selected));
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Future<void> delete(String albumId) => transaction(() async {
|
||||
// Remove all assets that are only in this particular album
|
||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||
@@ -335,4 +343,16 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
Future<int> getCount() {
|
||||
return _db.managers.localAlbumEntity.count();
|
||||
}
|
||||
|
||||
Future unlinkRemoteAlbum(String id) async {
|
||||
return _db.localAlbumEntity.update()
|
||||
..where((row) => row.id.equals(id))
|
||||
..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
|
||||
}
|
||||
|
||||
Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
|
||||
return _db.localAlbumEntity.update()
|
||||
..where((row) => row.id.equals(localAlbumId))
|
||||
..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
|
||||
final query = _db.remoteAlbumEntity.select()
|
||||
..where((row) => row.name.equals(albumName) & row.ownerId.equals(ownerId))
|
||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||
..limit(1);
|
||||
|
||||
return query.map((row) => row.toDto(ownerName: '', isShared: false)).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> create(RemoteAlbum album, List<String> assetIds) async {
|
||||
await _db.transaction(() async {
|
||||
final entity = RemoteAlbumEntityCompanion(
|
||||
@@ -321,6 +330,42 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
Future<int> getCount() {
|
||||
return _db.managers.remoteAlbumEntity.count();
|
||||
}
|
||||
|
||||
Future<List<String>> getLinkedAssetIds(String userId, String localAlbumId, String remoteAlbumId) async {
|
||||
// Find remote asset ids that:
|
||||
// 1. Belong to the provided local album (via local_album_asset_entity)
|
||||
// 2. Have been uploaded (i.e. a matching remote asset exists for the same checksum & owner)
|
||||
// 3. Are NOT already in the remote album (remote_album_asset_entity)
|
||||
final query = _db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([_db.remoteAssetEntity.id])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAssetEntity,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||
useColumns: false,
|
||||
),
|
||||
innerJoin(
|
||||
_db.localAlbumAssetEntity,
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
// Left join remote album assets to exclude those already in the remote album
|
||||
leftOuterJoin(
|
||||
_db.remoteAlbumAssetEntity,
|
||||
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id) &
|
||||
_db.remoteAlbumAssetEntity.albumId.equals(remoteAlbumId),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(
|
||||
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||
_db.localAlbumAssetEntity.albumId.equals(localAlbumId) &
|
||||
_db.remoteAlbumAssetEntity.assetId.isNull(), // only those not yet linked
|
||||
);
|
||||
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.id)!).get();
|
||||
}
|
||||
}
|
||||
|
||||
extension on RemoteAlbumEntityData {
|
||||
|
||||
@@ -16,7 +16,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
@@ -26,7 +25,6 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||
@@ -207,12 +205,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
// needs to be delayed so that EasyLocalization is working
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
ref.read(backgroundServiceProvider).disableService();
|
||||
ref.read(driftBackgroundUploadFgService).enableSyncService();
|
||||
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
|
||||
ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||
}
|
||||
ref.read(driftBackgroundUploadFgService).enable();
|
||||
} else {
|
||||
ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||
ref.read(driftBackgroundUploadFgService).disable();
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -43,12 +42,10 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
|
||||
await ref.read(backgroundSyncProvider).syncRemote();
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
await ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||
}
|
||||
|
||||
Future<void> stopBackup() async {
|
||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||
await ref.read(driftBackupProvider.notifier).cancel();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -26,10 +27,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
String _searchQuery = '';
|
||||
bool _isSearchMode = false;
|
||||
int _initialTotalAssetCount = 0;
|
||||
bool _hasPopped = false;
|
||||
late ValueNotifier<bool> _enableSyncUploadAlbum;
|
||||
late TextEditingController _searchController;
|
||||
late FocusNode _searchFocusNode;
|
||||
Future? _handleLinkedAlbumFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -44,6 +45,36 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||
}
|
||||
|
||||
Future<void> _handlePagePopped() async {
|
||||
final user = ref.read(currentUserProvider);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
final selectedAlbums = ref
|
||||
.read(backupAlbumProvider)
|
||||
.where((a) => a.backupSelection == BackupSelection.selected)
|
||||
.toList();
|
||||
|
||||
if (enableSyncUploadAlbum && selectedAlbums.isNotEmpty) {
|
||||
setState(() {
|
||||
_handleLinkedAlbumFuture = ref.read(syncLinkedAlbumServiceProvider).manageLinkedAlbums(selectedAlbums, user.id);
|
||||
});
|
||||
await _handleLinkedAlbumFuture;
|
||||
}
|
||||
|
||||
// Restart backup if total count changed and backup is enabled
|
||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
|
||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
if (totalChanged && isBackupEnabled) {
|
||||
await ref.read(driftBackupProvider.notifier).cancel();
|
||||
await ref.read(driftBackupProvider.notifier).startBackup(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_enableSyncUploadAlbum.dispose();
|
||||
@@ -65,42 +96,12 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
||||
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
||||
|
||||
// handleSyncAlbumToggle(bool isEnable) async {
|
||||
// if (isEnable) {
|
||||
// await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||
// for (final album in selectedBackupAlbums) {
|
||||
// await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
// There is an issue with Flutter where the pop event
|
||||
// can be triggered multiple times, so we guard it with _hasPopped
|
||||
if (didPop && !_hasPopped) {
|
||||
_hasPopped = true;
|
||||
|
||||
final currentUser = ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||
|
||||
if (currentTotalAssetCount != _initialTotalAssetCount) {
|
||||
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
if (!isBackupEnabled) {
|
||||
return;
|
||||
}
|
||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||
|
||||
backupNotifier.cancel().then((_) {
|
||||
backupNotifier.startBackup(currentUser.id);
|
||||
});
|
||||
}
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (!didPop) {
|
||||
await _handlePagePopped();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -139,103 +140,123 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
],
|
||||
elevation: 0,
|
||||
),
|
||||
body: CustomScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_selection_info",
|
||||
style: context.textTheme.titleSmall,
|
||||
).t(context: context),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_selection_info",
|
||||
style: context.textTheme.titleSmall,
|
||||
).t(context: context),
|
||||
),
|
||||
|
||||
// Selected Album Chips
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Wrap(
|
||||
children: [
|
||||
_SelectedAlbumNameChips(selectedBackupAlbums: selectedBackupAlbums),
|
||||
_ExcludedAlbumNameChips(excludedBackupAlbums: excludedBackupAlbums),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// SettingsSwitchListTile(
|
||||
// valueNotifier: _enableSyncUploadAlbum,
|
||||
// title: "sync_albums".t(context: context),
|
||||
// subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
// titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
// subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
||||
// onChanged: handleSyncAlbumToggle,
|
||||
// ),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
||||
style: context.textTheme.titleSmall,
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_albums_tap",
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
||||
).t(context: context),
|
||||
),
|
||||
trailing: IconButton(
|
||||
splashRadius: 16,
|
||||
icon: Icon(Icons.info, size: 20, color: context.primaryColor),
|
||||
onPressed: () {
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 5,
|
||||
title: Text(
|
||||
'backup_album_selection_page_selection_info',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).t(context: context),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
const Text(
|
||||
'backup_album_selection_page_assets_scatter',
|
||||
style: TextStyle(fontSize: 14),
|
||||
).t(context: context),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Selected Album Chips
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Wrap(
|
||||
children: [
|
||||
_SelectedAlbumNameChips(selectedBackupAlbums: selectedBackupAlbums),
|
||||
_ExcludedAlbumNameChips(excludedBackupAlbums: excludedBackupAlbums),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
||||
style: context.textTheme.titleSmall,
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
"backup_album_selection_page_albums_tap",
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor),
|
||||
).t(context: context),
|
||||
),
|
||||
trailing: IconButton(
|
||||
splashRadius: 16,
|
||||
icon: Icon(Icons.info, size: 20, color: context.primaryColor),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
elevation: 5,
|
||||
title: Text(
|
||||
'backup_album_selection_page_selection_info',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).t(context: context),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
const Text(
|
||||
'backup_album_selection_page_assets_scatter',
|
||||
style: TextStyle(fontSize: 14),
|
||||
).t(context: context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (Platform.isAndroid)
|
||||
_SelectAllButton(filteredAlbums: filteredAlbums, selectedBackupAlbums: selectedBackupAlbums),
|
||||
],
|
||||
if (Platform.isAndroid)
|
||||
_SelectAllButton(filteredAlbums: filteredAlbums, selectedBackupAlbums: selectedBackupAlbums),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.crossAxisExtent > 600) {
|
||||
return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||
} else {
|
||||
return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_handleLinkedAlbumFuture != null)
|
||||
FutureBuilder(
|
||||
future: _handleLinkedAlbumFuture,
|
||||
builder: (context, snapshot) {
|
||||
return SizedBox(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
child: Container(
|
||||
color: context.scaffoldBackgroundColor.withValues(alpha: 0.8),
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const CircularProgressIndicator(strokeWidth: 4),
|
||||
Text("Creating linked albums...", style: context.textTheme.labelLarge),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.crossAxisExtent > 600) {
|
||||
return _AlbumSelectionGrid(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||
} else {
|
||||
return _AlbumSelectionList(filteredAlbums: filteredAlbums, searchQuery: _searchQuery);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -79,7 +79,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||
await ref.read(driftBackgroundUploadFgService).disable();
|
||||
}
|
||||
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
|
||||
@@ -2,8 +2,10 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
@@ -21,14 +23,23 @@ class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||
|
||||
class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
final log = Logger("SplashScreenPage");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession());
|
||||
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||
|
||||
lockManager.requestHolderToClose();
|
||||
lockManager
|
||||
.acquireLock()
|
||||
.timeout(const Duration(seconds: 5))
|
||||
.whenComplete(
|
||||
() => ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession()),
|
||||
);
|
||||
}
|
||||
|
||||
void logConnectionInfo(String? endpoint) {
|
||||
|
||||
@@ -11,6 +11,8 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
@@ -136,6 +138,10 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
EventStream.shared.emit(const ScrollToTopEvent());
|
||||
}
|
||||
|
||||
if (index == 0) {
|
||||
ref.invalidate(driftMemoryFutureProvider);
|
||||
}
|
||||
|
||||
// On Search page tapped
|
||||
if (router.activeIndex == 1 && index == 1) {
|
||||
ref.read(searchInputFocusProvider).requestFocus();
|
||||
@@ -146,8 +152,10 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
// Library page
|
||||
if (index == 3) {
|
||||
ref.invalidate(localAlbumProvider);
|
||||
ref.invalidate(driftGetAllPeopleProvider);
|
||||
}
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
62
mobile/lib/platform/background_worker_api.g.dart
generated
62
mobile/lib/platform/background_worker_api.g.dart
generated
@@ -59,9 +59,9 @@ class BackgroundWorkerFgHostApi {
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> enableSyncWorker() async {
|
||||
Future<void> enable() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix';
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -82,32 +82,9 @@ class BackgroundWorkerFgHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enableUploadWorker() async {
|
||||
Future<void> disable() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disableUploadWorker() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -192,8 +169,6 @@ class BackgroundWorkerBgHostApi {
|
||||
abstract class BackgroundWorkerFlutterApi {
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
Future<void> onLocalSync(int? maxSeconds);
|
||||
|
||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||
|
||||
Future<void> onAndroidUpload();
|
||||
@@ -206,35 +181,6 @@ abstract class BackgroundWorkerFlutterApi {
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
assert(
|
||||
message != null,
|
||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync was null.',
|
||||
);
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_maxSeconds = (args[0] as int?);
|
||||
try {
|
||||
await api.onLocalSync(arg_maxSeconds);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
@@ -38,14 +39,14 @@ class DriftPlacePage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaceSliverAppBar extends StatelessWidget {
|
||||
class _PlaceSliverAppBar extends HookWidget {
|
||||
const _PlaceSliverAppBar({required this.search});
|
||||
|
||||
final ValueNotifier<String?> search;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchFocusNode = FocusNode();
|
||||
final searchFocusNode = useFocusNode();
|
||||
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/album_filter.utils.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
@@ -39,8 +40,12 @@ class AlbumSelector extends ConsumerStatefulWidget {
|
||||
class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
bool isGrid = false;
|
||||
final searchController = TextEditingController();
|
||||
QuickFilterMode filterMode = QuickFilterMode.all;
|
||||
final searchFocusNode = FocusNode();
|
||||
List<RemoteAlbum> sortedAlbums = [];
|
||||
List<RemoteAlbum> shownAlbums = [];
|
||||
|
||||
AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
|
||||
AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -52,7 +57,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
});
|
||||
|
||||
searchController.addListener(() {
|
||||
onSearch(searchController.text, filterMode);
|
||||
onSearch(searchController.text, filter.mode);
|
||||
});
|
||||
|
||||
searchFocusNode.addListener(() {
|
||||
@@ -62,9 +67,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
});
|
||||
}
|
||||
|
||||
void onSearch(String searchTerm, QuickFilterMode sortMode) {
|
||||
void onSearch(String searchTerm, QuickFilterMode filterMode) {
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
ref.read(remoteAlbumProvider.notifier).searchAlbums(searchTerm, userId, sortMode);
|
||||
filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode);
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
@@ -77,17 +84,60 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
});
|
||||
}
|
||||
|
||||
void changeFilter(QuickFilterMode sortMode) {
|
||||
void changeFilter(QuickFilterMode mode) {
|
||||
setState(() {
|
||||
filterMode = sortMode;
|
||||
filter = filter.copyWith(mode: mode);
|
||||
});
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> changeSort(AlbumSort sort) async {
|
||||
setState(() {
|
||||
this.sort = sort;
|
||||
});
|
||||
|
||||
await sortAlbums();
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
setState(() {
|
||||
filterMode = QuickFilterMode.all;
|
||||
filter = filter.copyWith(mode: QuickFilterMode.all, query: null);
|
||||
searchController.clear();
|
||||
ref.read(remoteAlbumProvider.notifier).clearSearch();
|
||||
});
|
||||
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> sortAlbums() async {
|
||||
final sorted = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse);
|
||||
|
||||
setState(() {
|
||||
sortedAlbums = sorted;
|
||||
});
|
||||
|
||||
// we need to re-filter the albums after sorting
|
||||
// so shownAlbums gets updated
|
||||
filterAlbums();
|
||||
}
|
||||
|
||||
Future<void> filterAlbums() async {
|
||||
if (filter.query == null) {
|
||||
setState(() {
|
||||
shownAlbums = sortedAlbums;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final filteredAlbums = ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode);
|
||||
|
||||
setState(() {
|
||||
shownAlbums = filteredAlbums;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,36 +150,41 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final albums = ref.watch(remoteAlbumProvider.select((s) => s.filteredAlbums));
|
||||
|
||||
final userId = ref.watch(currentUserProvider)?.id;
|
||||
|
||||
// refilter and sort when albums change
|
||||
ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async {
|
||||
await sortAlbums();
|
||||
});
|
||||
|
||||
return MultiSliver(
|
||||
children: [
|
||||
_SearchBar(
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearch: onSearch,
|
||||
filterMode: filterMode,
|
||||
filterMode: filter.mode,
|
||||
onClearSearch: clearSearch,
|
||||
),
|
||||
_QuickFilterButtonRow(
|
||||
filterMode: filterMode,
|
||||
filterMode: filter.mode,
|
||||
onChangeFilter: changeFilter,
|
||||
onSearch: onSearch,
|
||||
searchController: searchController,
|
||||
),
|
||||
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode),
|
||||
_QuickSortAndViewMode(isGrid: isGrid, onToggleViewMode: toggleViewMode, onSortChanged: changeSort),
|
||||
isGrid
|
||||
? _AlbumGrid(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: albums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected)
|
||||
: _AlbumList(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends ConsumerStatefulWidget {
|
||||
const _SortButton();
|
||||
const _SortButton(this.onSortChanged);
|
||||
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
|
||||
@override
|
||||
ConsumerState<_SortButton> createState() => _SortButtonState();
|
||||
@@ -148,15 +203,15 @@ class _SortButtonState extends ConsumerState<_SortButton> {
|
||||
albumSortIsReverse = !albumSortIsReverse;
|
||||
isSorting = true;
|
||||
});
|
||||
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
||||
} else {
|
||||
setState(() {
|
||||
albumSortOption = sortMode;
|
||||
isSorting = true;
|
||||
});
|
||||
await ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(sortMode, isReverse: albumSortIsReverse);
|
||||
}
|
||||
|
||||
await widget.onSortChanged.call(AlbumSort(mode: albumSortOption, isReverse: albumSortIsReverse));
|
||||
|
||||
setState(() {
|
||||
isSorting = false;
|
||||
});
|
||||
@@ -394,10 +449,11 @@ class _QuickFilterButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _QuickSortAndViewMode extends StatelessWidget {
|
||||
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode});
|
||||
const _QuickSortAndViewMode({required this.isGrid, required this.onToggleViewMode, required this.onSortChanged});
|
||||
|
||||
final bool isGrid;
|
||||
final VoidCallback onToggleViewMode;
|
||||
final Future<void> Function(AlbumSort) onSortChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -407,7 +463,7 @@ class _QuickSortAndViewMode extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const _SortButton(),
|
||||
_SortButton(onSortChanged),
|
||||
IconButton(
|
||||
icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24),
|
||||
onPressed: onToggleViewMode,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
@@ -129,6 +130,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
reloadSubscription?.cancel();
|
||||
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||
_nextPreCacheStream?.removeListener(_dummyListener);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -596,6 +598,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
// Rebuild the widget when the asset viewer state changes
|
||||
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||
ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||
ref.watch(isPlayingMotionVideoProvider);
|
||||
@@ -612,6 +615,15 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for control visibility changes and change system UI mode accordingly
|
||||
ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async {
|
||||
if (showingControls) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
}
|
||||
});
|
||||
|
||||
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||
|
||||
@@ -62,7 +62,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
duration: Durations.short2,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Durations.short4,
|
||||
child: isSheetOpen || isReadonlyModeEnabled
|
||||
child: isSheetOpen
|
||||
? const SizedBox.shrink()
|
||||
: Theme(
|
||||
data: context.themeData.copyWith(
|
||||
@@ -72,14 +72,14 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
height: context.padding.bottom + (asset.isVideo ? 160 : 90),
|
||||
color: Colors.black.withAlpha(125),
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (asset.isVideo) const VideoControls(),
|
||||
if (!isInLockedView) Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
if (!isInLockedView && !isReadonlyModeEnabled)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
@@ -16,22 +18,74 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||
final RemoteAlbum album;
|
||||
const RemoteAlbumBottomSheet({super.key, required this.album});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<RemoteAlbumBottomSheet> createState() => _RemoteAlbumBottomSheetState();
|
||||
}
|
||||
|
||||
class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet> {
|
||||
late DraggableScrollableController sheetController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sheetController = DraggableScrollableController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
sheetController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
|
||||
|
||||
if (addedCount != selectedAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
controller: sheetController,
|
||||
initialChildSize: 0.45,
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
@@ -52,7 +106,11 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
const UploadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: album.id),
|
||||
RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
|
||||
],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,10 +47,12 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
MapLibreMapController? mapController;
|
||||
final _reloadMutex = AsyncMutex();
|
||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncer.dispose();
|
||||
bottomSheetOffset.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -157,8 +159,8 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
return Stack(
|
||||
children: [
|
||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||
_MyLocationButton(onZoomToLocation: onZoomToLocation),
|
||||
const MapBottomSheet(),
|
||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -191,21 +193,53 @@ class _Map extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MyLocationButton extends StatelessWidget {
|
||||
const _MyLocationButton({required this.onZoomToLocation});
|
||||
class _DynamicBottomSheet extends StatefulWidget {
|
||||
final ValueNotifier<double> bottomSheetOffset;
|
||||
|
||||
final VoidCallback onZoomToLocation;
|
||||
const _DynamicBottomSheet({required this.bottomSheetOffset});
|
||||
|
||||
@override
|
||||
State<_DynamicBottomSheet> createState() => _DynamicBottomSheetState();
|
||||
}
|
||||
|
||||
class _DynamicBottomSheetState extends State<_DynamicBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: context.padding.bottom + 16,
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
return NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: (notification) {
|
||||
widget.bottomSheetOffset.value = notification.extent;
|
||||
return true;
|
||||
},
|
||||
child: const MapBottomSheet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DynamicMyLocationButton extends StatelessWidget {
|
||||
const _DynamicMyLocationButton({required this.onZoomToLocation, required this.bottomSheetOffset});
|
||||
|
||||
final VoidCallback onZoomToLocation;
|
||||
final ValueNotifier<double> bottomSheetOffset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: bottomSheetOffset,
|
||||
builder: (context, offset, child) {
|
||||
return Positioned(
|
||||
right: 16,
|
||||
bottom: context.height * (offset - 0.02) + context.padding.bottom,
|
||||
child: AnimatedOpacity(
|
||||
opacity: offset < 0.8 ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(shape: const CircleBorder()),
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
|
||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||
/// for quick navigation of the BoxScrollView.
|
||||
@@ -74,6 +75,7 @@ List<_Segment> _buildSegments({required List<Segment> layoutSegments, required d
|
||||
}
|
||||
|
||||
class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixin {
|
||||
String? _lastLabel;
|
||||
double _thumbTopOffset = 0.0;
|
||||
bool _isDragging = false;
|
||||
List<_Segment> _segments = [];
|
||||
@@ -172,6 +174,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
_isDragging = true;
|
||||
_labelAnimationController.forward();
|
||||
_fadeOutTimer?.cancel();
|
||||
_lastLabel = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,6 +192,11 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
|
||||
if (nearestMonthSegment != null) {
|
||||
_snapToSegment(nearestMonthSegment);
|
||||
final label = nearestMonthSegment.scrollLabel;
|
||||
if (_lastLabel != label) {
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
_lastLabel = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
@@ -33,6 +33,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
final Ref _ref;
|
||||
bool _wasPaused = false;
|
||||
|
||||
// Add operation coordination
|
||||
Completer<void>? _resumeOperation;
|
||||
Completer<void>? _pauseOperation;
|
||||
|
||||
final _log = Logger("AppLifeCycleNotifier");
|
||||
|
||||
AppLifeCycleNotifier(this._ref) : super(AppLifeCycleEnum.active);
|
||||
|
||||
AppLifeCycleEnum getAppState() {
|
||||
@@ -42,6 +48,32 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
void handleAppResume() async {
|
||||
state = AppLifeCycleEnum.resumed;
|
||||
|
||||
// Prevent overlapping resume operations
|
||||
if (_resumeOperation != null && !_resumeOperation!.isCompleted) {
|
||||
await _resumeOperation!.future;
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any ongoing pause operation
|
||||
if (_pauseOperation != null && !_pauseOperation!.isCompleted) {
|
||||
_pauseOperation!.complete();
|
||||
}
|
||||
|
||||
_resumeOperation = Completer<void>();
|
||||
|
||||
try {
|
||||
await _performResume();
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("Error during app resume", e, stackTrace);
|
||||
} finally {
|
||||
if (!_resumeOperation!.isCompleted) {
|
||||
_resumeOperation!.complete();
|
||||
}
|
||||
_resumeOperation = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performResume() async {
|
||||
// no need to resume because app was never really paused
|
||||
if (!_wasPaused) return;
|
||||
_wasPaused = false;
|
||||
@@ -52,9 +84,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
if (isAuthenticated) {
|
||||
// switch endpoint if needed
|
||||
final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
||||
if (kDebugMode) {
|
||||
debugPrint("Using server URL: $endpoint");
|
||||
}
|
||||
_log.info("Using server URL: $endpoint");
|
||||
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
final permission = _ref.watch(galleryPermissionNotifier);
|
||||
@@ -80,40 +110,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
_ref.read(backupProvider.notifier).cancelBackup();
|
||||
|
||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||
// Ensure proper cleanup before starting new background tasks
|
||||
try {
|
||||
await Future.wait([
|
||||
Future(() async {
|
||||
await backgroundManager.syncLocal();
|
||||
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
||||
// Check if app is still active before hashing
|
||||
if ([AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state)) {
|
||||
await backgroundManager.hashAssets();
|
||||
}
|
||||
}),
|
||||
backgroundManager.syncRemote(),
|
||||
]).then((_) async {
|
||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
Logger("AppLifeCycleNotifier").severe("Error during background sync", e, stackTrace);
|
||||
}
|
||||
_ref.read(websocketProvider.notifier).connect();
|
||||
await _handleBetaTimelineResume();
|
||||
}
|
||||
|
||||
_ref.read(websocketProvider.notifier).connect();
|
||||
|
||||
await _ref.read(notificationPermissionProvider.notifier).getNotificationPermission();
|
||||
|
||||
await _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus();
|
||||
@@ -125,21 +125,166 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleBetaTimelineResume() async {
|
||||
_ref.read(backupProvider.notifier).cancelBackup();
|
||||
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||
|
||||
// Give isolates time to complete any ongoing database transactions
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
lockManager.requestHolderToClose();
|
||||
|
||||
// Add timeout to prevent deadlock on lock acquisition
|
||||
try {
|
||||
await lockManager.acquireLock().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
_log.warning("Lock acquisition timed out, proceeding without lock");
|
||||
throw TimeoutException("Lock acquisition timed out", const Duration(seconds: 10));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to acquire lock: $e");
|
||||
return;
|
||||
}
|
||||
|
||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
|
||||
try {
|
||||
// Run operations sequentially with state checks and error handling for each
|
||||
if (_shouldContinueOperation()) {
|
||||
try {
|
||||
await backgroundManager.syncLocal();
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed syncLocal: $e", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if app is still active before hashing
|
||||
if (_shouldContinueOperation()) {
|
||||
try {
|
||||
await backgroundManager.hashAssets();
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed hashAssets: $e", e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if app is still active before remote sync
|
||||
if (_shouldContinueOperation()) {
|
||||
try {
|
||||
await backgroundManager.syncRemote();
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed syncRemote: $e", e, stackTrace);
|
||||
}
|
||||
|
||||
if (isAlbumLinkedSyncEnable && _shouldContinueOperation()) {
|
||||
try {
|
||||
await backgroundManager.syncLinkedAlbum();
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed syncLinkedAlbum: $e", e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle backup resume only if still active
|
||||
if (_shouldContinueOperation()) {
|
||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser != null) {
|
||||
try {
|
||||
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
_log.fine("Completed backup resume");
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed backup resume: $e", e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("Error during background sync", e, stackTrace);
|
||||
} finally {
|
||||
// Ensure lock is released even if operations fail
|
||||
try {
|
||||
lockManager.releaseLock();
|
||||
_log.fine("Lock released after background sync operations");
|
||||
} catch (lockError) {
|
||||
_log.warning("Failed to release lock after error: $lockError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to check if operations should continue
|
||||
bool _shouldContinueOperation() {
|
||||
return [AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state) &&
|
||||
(_resumeOperation?.isCompleted == false || _resumeOperation == null);
|
||||
}
|
||||
|
||||
void handleAppInactivity() {
|
||||
state = AppLifeCycleEnum.inactive;
|
||||
// do not stop/clean up anything on inactivity: issued on every orientation change
|
||||
}
|
||||
|
||||
void handleAppPause() {
|
||||
Future<void> handleAppPause() async {
|
||||
state = AppLifeCycleEnum.paused;
|
||||
_wasPaused = true;
|
||||
|
||||
// Prevent overlapping pause operations
|
||||
if (_pauseOperation != null && !_pauseOperation!.isCompleted) {
|
||||
await _pauseOperation!.future;
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any ongoing resume operation
|
||||
if (_resumeOperation != null && !_resumeOperation!.isCompleted) {
|
||||
_resumeOperation!.complete();
|
||||
}
|
||||
|
||||
_pauseOperation = Completer<void>();
|
||||
|
||||
try {
|
||||
await _performPause();
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe("Error during app pause", e, stackTrace);
|
||||
} finally {
|
||||
if (!_pauseOperation!.isCompleted) {
|
||||
_pauseOperation!.complete();
|
||||
}
|
||||
_pauseOperation = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performPause() async {
|
||||
if (_ref.read(authProvider).isAuthenticated) {
|
||||
if (!Store.isBetaTimelineEnabled) {
|
||||
// Do not cancel backup if manual upload is in progress
|
||||
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
|
||||
_ref.read(backupProvider.notifier).cancelBackup();
|
||||
}
|
||||
} else {
|
||||
final backgroundManager = _ref.read(backgroundSyncProvider);
|
||||
|
||||
// Cancel operations with extended timeout to allow database transactions to complete
|
||||
try {
|
||||
await Future.wait([
|
||||
backgroundManager.cancel().timeout(const Duration(seconds: 10)),
|
||||
backgroundManager.cancelLocal().timeout(const Duration(seconds: 10)),
|
||||
]).timeout(const Duration(seconds: 15));
|
||||
|
||||
// Give additional time for isolates to clean up database connections
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
} catch (e) {
|
||||
_log.warning("Timeout during background cancellation: $e");
|
||||
}
|
||||
|
||||
// Always release the lock, even if cancellation failed
|
||||
try {
|
||||
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
||||
} catch (e) {
|
||||
_log.warning("Failed to release lock on pause: $e");
|
||||
}
|
||||
}
|
||||
|
||||
_ref.read(websocketProvider.notifier).disconnect();
|
||||
@@ -147,9 +292,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
|
||||
try {
|
||||
LogService.I.flush();
|
||||
} catch (e) {
|
||||
// Ignore flush errors during pause
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> handleAppDetached() async {
|
||||
@@ -158,9 +301,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
// Flush logs before closing database
|
||||
try {
|
||||
LogService.I.flush();
|
||||
} catch (e) {
|
||||
// Ignore flush errors during shutdown
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Close Isar database safely
|
||||
try {
|
||||
@@ -168,20 +309,17 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
if (isar != null && isar.isOpen) {
|
||||
await isar.close();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore close errors during shutdown
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
||||
return;
|
||||
}
|
||||
|
||||
// no guarantee this is called at all
|
||||
try {
|
||||
_ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
} catch (e) {
|
||||
// Ignore errors during shutdown
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void handleAppHidden() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
|
||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||
|
||||
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||
@@ -18,3 +19,7 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||
ref.onDispose(manager.cancel);
|
||||
return manager;
|
||||
});
|
||||
|
||||
final isolateLockManagerProvider = Provider.family<IsolateLockManager, String>((ref, name) {
|
||||
return IsolateLockManager(portName: name);
|
||||
});
|
||||
|
||||
@@ -12,43 +12,42 @@ import 'album.provider.dart';
|
||||
|
||||
class RemoteAlbumState {
|
||||
final List<RemoteAlbum> albums;
|
||||
final List<RemoteAlbum> filteredAlbums;
|
||||
|
||||
const RemoteAlbumState({required this.albums, List<RemoteAlbum>? filteredAlbums})
|
||||
: filteredAlbums = filteredAlbums ?? albums;
|
||||
const RemoteAlbumState({required this.albums});
|
||||
|
||||
RemoteAlbumState copyWith({List<RemoteAlbum>? albums, List<RemoteAlbum>? filteredAlbums}) {
|
||||
return RemoteAlbumState(albums: albums ?? this.albums, filteredAlbums: filteredAlbums ?? this.filteredAlbums);
|
||||
RemoteAlbumState copyWith({List<RemoteAlbum>? albums}) {
|
||||
return RemoteAlbumState(albums: albums ?? this.albums);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'RemoteAlbumState(albums: ${albums.length}, filteredAlbums: ${filteredAlbums.length})';
|
||||
String toString() => 'RemoteAlbumState(albums: ${albums.length})';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant RemoteAlbumState other) {
|
||||
if (identical(this, other)) return true;
|
||||
final listEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return listEquals(other.albums, albums) && listEquals(other.filteredAlbums, filteredAlbums);
|
||||
return listEquals(other.albums, albums);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => albums.hashCode ^ filteredAlbums.hashCode;
|
||||
int get hashCode => albums.hashCode;
|
||||
}
|
||||
|
||||
class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
late RemoteAlbumService _remoteAlbumService;
|
||||
final _logger = Logger('RemoteAlbumNotifier');
|
||||
|
||||
@override
|
||||
RemoteAlbumState build() {
|
||||
_remoteAlbumService = ref.read(remoteAlbumServiceProvider);
|
||||
return const RemoteAlbumState(albums: [], filteredAlbums: []);
|
||||
return const RemoteAlbumState(albums: []);
|
||||
}
|
||||
|
||||
Future<List<RemoteAlbum>> _getAll() async {
|
||||
try {
|
||||
final albums = await _remoteAlbumService.getAll();
|
||||
state = state.copyWith(albums: albums, filteredAlbums: albums);
|
||||
state = state.copyWith(albums: albums);
|
||||
return albums;
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to fetch albums', error, stack);
|
||||
@@ -60,19 +59,21 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
await _getAll();
|
||||
}
|
||||
|
||||
void searchAlbums(String query, String? userId, [QuickFilterMode filterMode = QuickFilterMode.all]) {
|
||||
final filtered = _remoteAlbumService.searchAlbums(state.albums, query, userId, filterMode);
|
||||
|
||||
state = state.copyWith(filteredAlbums: filtered);
|
||||
List<RemoteAlbum> searchAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
String query,
|
||||
String? userId, [
|
||||
QuickFilterMode filterMode = QuickFilterMode.all,
|
||||
]) {
|
||||
return _remoteAlbumService.searchAlbums(albums, query, userId, filterMode);
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
state = state.copyWith(filteredAlbums: state.albums);
|
||||
}
|
||||
|
||||
Future<void> sortFilteredAlbums(RemoteAlbumSortMode sortMode, {bool isReverse = false}) async {
|
||||
final sortedAlbums = await _remoteAlbumService.sortAlbums(state.filteredAlbums, sortMode, isReverse: isReverse);
|
||||
state = state.copyWith(filteredAlbums: sortedAlbums);
|
||||
Future<List<RemoteAlbum>> sortAlbums(
|
||||
List<RemoteAlbum> albums,
|
||||
RemoteAlbumSortMode sortMode, {
|
||||
bool isReverse = false,
|
||||
}) async {
|
||||
return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum?> createAlbum({
|
||||
@@ -83,7 +84,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
try {
|
||||
final album = await _remoteAlbumService.createAlbum(title: title, description: description, assetIds: assetIds);
|
||||
|
||||
state = state.copyWith(albums: [...state.albums, album], filteredAlbums: [...state.filteredAlbums, album]);
|
||||
state = state.copyWith(albums: [...state.albums, album]);
|
||||
|
||||
return album;
|
||||
} catch (error, stack) {
|
||||
@@ -114,11 +115,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
return album.id == albumId ? updatedAlbum : album;
|
||||
}).toList();
|
||||
|
||||
final updatedFilteredAlbums = state.filteredAlbums.map((album) {
|
||||
return album.id == albumId ? updatedAlbum : album;
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||
state = state.copyWith(albums: updatedAlbums);
|
||||
|
||||
return updatedAlbum;
|
||||
} catch (error, stack) {
|
||||
@@ -139,9 +136,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
await _remoteAlbumService.deleteAlbum(albumId);
|
||||
|
||||
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
||||
|
||||
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||
state = state.copyWith(albums: updatedAlbums);
|
||||
}
|
||||
|
||||
Future<List<RemoteAsset>> getAssets(String albumId) {
|
||||
@@ -164,9 +159,7 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
await _remoteAlbumService.removeUser(albumId, userId: userId);
|
||||
|
||||
final updatedAlbums = state.albums.where((album) => album.id != albumId).toList();
|
||||
final updatedFilteredAlbums = state.filteredAlbums.where((album) => album.id != albumId).toList();
|
||||
|
||||
state = state.copyWith(albums: updatedAlbums, filteredAlbums: updatedFilteredAlbums);
|
||||
state = state.copyWith(albums: updatedAlbums);
|
||||
}
|
||||
|
||||
Future<void> setActivityStatus(String albumId, bool enabled) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
// import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
@@ -323,7 +322,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
}
|
||||
|
||||
try {
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()));
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
|
||||
return _ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
_log.severe("Error processing batched AssetUploadReadyV1 events: $error");
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ class PartnerApiRepository extends ApiRepository {
|
||||
}
|
||||
|
||||
Future<UserDto> create(String id) async {
|
||||
final dto = await checkNull(_api.createPartner(id));
|
||||
final dto = await checkNull(_api.createPartnerDeprecated(id));
|
||||
return UserConverter.fromPartnerDto(dto);
|
||||
}
|
||||
|
||||
Future<void> delete(String id) => _api.removePartner(id);
|
||||
|
||||
Future<UserDto> update(String id, {required bool inTimeline}) async {
|
||||
final dto = await checkNull(_api.updatePartner(id, UpdatePartnerDto(inTimeline: inTimeline)));
|
||||
final dto = await checkNull(_api.updatePartner(id, PartnerUpdateDto(inTimeline: inTimeline)));
|
||||
return UserConverter.fromPartnerDto(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +282,8 @@ class UploadService {
|
||||
|
||||
return buildUploadTask(
|
||||
file,
|
||||
createdAt: asset.createdAt,
|
||||
modifiedAt: asset.updatedAt,
|
||||
originalFileName: originalFileName,
|
||||
deviceAssetId: asset.id,
|
||||
metadata: metadata,
|
||||
@@ -309,6 +311,8 @@ class UploadService {
|
||||
|
||||
return buildUploadTask(
|
||||
file,
|
||||
createdAt: asset.createdAt,
|
||||
modifiedAt: asset.updatedAt,
|
||||
originalFileName: asset.name,
|
||||
deviceAssetId: asset.id,
|
||||
fields: fields,
|
||||
@@ -334,6 +338,8 @@ class UploadService {
|
||||
Future<UploadTask> buildUploadTask(
|
||||
File file, {
|
||||
required String group,
|
||||
required DateTime createdAt,
|
||||
required DateTime modifiedAt,
|
||||
Map<String, String>? fields,
|
||||
String? originalFileName,
|
||||
String? deviceAssetId,
|
||||
@@ -347,15 +353,12 @@ class UploadService {
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
final deviceId = Store.get(StoreKey.deviceId);
|
||||
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
|
||||
final stats = await file.stat();
|
||||
final fileCreatedAt = stats.changed;
|
||||
final fileModifiedAt = stats.modified;
|
||||
final fieldsMap = {
|
||||
'filename': originalFileName ?? filename,
|
||||
'deviceAssetId': deviceAssetId ?? '',
|
||||
'deviceId': deviceId,
|
||||
'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
|
||||
'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
|
||||
'fileCreatedAt': createdAt.toUtc().toIso8601String(),
|
||||
'fileModifiedAt': modifiedAt.toUtc().toIso8601String(),
|
||||
'isFavorite': isFavorite?.toString() ?? 'false',
|
||||
'duration': '0',
|
||||
if (fields != null) ...fields,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user