Compare commits

..

1 Commits

Author SHA1 Message Date
Thomas Way 0651612d46 fix(mobile): fix stale refs in use timer
The timer hook preserved the values of the original local variables,
which caused issues when hiding controls for videos. The callback can be
changed so that it always sees the latest value with useRef, and it can
be simplified significantly using a function rather than state class.
2026-04-05 04:03:32 +01:00
79 changed files with 843 additions and 1256 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.7.0",
"version": "2.6.3",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
+1 -1
View File
@@ -1,5 +1,5 @@
[tools]
terragrunt = "0.99.5"
terragrunt = "0.99.4"
opentofu = "1.11.5"
[tasks."tg:fmt"]
+1 -1
View File
@@ -156,7 +156,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
healthcheck:
test: redis-cli ping || exit 1
+2 -2
View File
@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:dda13e28bf95a5e5ca5b8ed56852006094c1c8e8871d9c9dbeed30aa6e55271f
image: prom/prometheus@sha256:4a61322ac1103a0e3aea2a61ef1718422a48fa046441f299d71e660a3bc71ae9
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
+1 -1
View File
@@ -61,7 +61,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
user: '1000:1000'
security_opt:
- no-new-privileges:true
+1 -1
View File
@@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
healthcheck:
test: redis-cli ping || exit 1
restart: always
+14 -14
View File
@@ -28,17 +28,17 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
## Video formats
| Format | Extension(s) | Supported? | Notes |
| :---------- | :-------------------------- | :----------------: | :---- |
| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | |
| `AVI` | `.avi` | :white_check_mark: | |
| `FLV` | `.flv` | :white_check_mark: | |
| `M4V` | `.m4v` | :white_check_mark: | |
| `MATROSKA` | `.mkv` | :white_check_mark: | |
| `MP2T` | `.mts` `.m2ts` `.m2t` `.ts` | :white_check_mark: | |
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
| `MXF` | `.mxf` | :white_check_mark: | |
| `QUICKTIME` | `.mov` | :white_check_mark: | |
| `WEBM` | `.webm` | :white_check_mark: | |
| `WMV` | `.wmv` | :white_check_mark: | |
| Format | Extension(s) | Supported? | Notes |
| :---------- | :-------------------- | :----------------: | :---- |
| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | |
| `AVI` | `.avi` | :white_check_mark: | |
| `FLV` | `.flv` | :white_check_mark: | |
| `M4V` | `.m4v` | :white_check_mark: | |
| `MATROSKA` | `.mkv` | :white_check_mark: | |
| `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | |
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
| `MXF` | `.mxf` | :white_check_mark: | |
| `QUICKTIME` | `.mov` | :white_check_mark: | |
| `WEBM` | `.webm` | :white_check_mark: | |
| `WMV` | `.wmv` | :white_check_mark: | |
+2 -2
View File
@@ -30,8 +30,8 @@
"postcss": "^8.4.25",
"prism-react-renderer": "^2.3.1",
"raw-loader": "^4.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tailwindcss": "^3.2.4",
"url": "^0.11.0"
},
-4
View File
@@ -1,8 +1,4 @@
[
{
"label": "v2.7.0",
"url": "https://docs.v2.7.0.archive.immich.app"
},
{
"label": "v2.6.3",
"url": "https://docs.v2.6.3.archive.immich.app"
+1 -1
View File
@@ -44,7 +44,7 @@ services:
redis:
container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
healthcheck:
test: redis-cli ping || exit 1
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.7.0",
"version": "2.6.3",
"description": "",
"main": "index.js",
"type": "module",
@@ -35,7 +35,7 @@
"@types/node": "^24.12.0",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^7.0.0",
"@types/supertest": "^6.0.2",
"dotenv": "^17.2.3",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
-1
View File
@@ -173,7 +173,6 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
'.mpeg',
'.mpg',
'.mts',
'.ts',
'.vob',
'.webm',
'.wmv',
@@ -6,7 +6,6 @@ import {
generateTimelineData,
TimelineAssetConfig,
TimelineData,
toAssetResponseDto,
} from 'src/ui/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
@@ -31,10 +30,6 @@ test.describe('search gallery-viewer', () => {
};
test.beforeAll(async () => {
test.fail(
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS !== '1',
'This test requires env var: PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1',
);
adminUserId = faker.string.uuid();
testContext.adminId = adminUserId;
timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId });
@@ -49,10 +44,7 @@ test.describe('search gallery-viewer', () => {
await context.route('**/api/search/metadata', async (route, request) => {
if (request.method() === 'POST') {
const searchAssets = assets
.slice(0, 5)
.filter((asset) => !changes.assetDeletions.includes(asset.id))
.map((asset) => toAssetResponseDto(asset));
const searchAssets = assets.slice(0, 5).filter((asset) => !changes.assetDeletions.includes(asset.id));
return route.fulfill({
status: 200,
contentType: 'application/json',
-9
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "إنشاء رابط للمشاركة",
"create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة",
"create_new": "انشاء جديد",
"create_new_face": "إنشاء وجه جديد",
"create_new_person": "إنشاء شخص جديد",
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
"create_new_user": "إنشاء مستخدم جديد",
"create_person": "إنشاء شخص",
"create_person_subtitle": "أضف اسماً للوجه المحدد لإنشاء الشخص الجديد والإشارة إليه",
"create_shared_album_page_share_add_assets": "إضافة الأصول",
"create_shared_album_page_share_select_photos": "حدد الصور",
"create_shared_link": "انشاء رابط مشترك",
@@ -895,8 +892,6 @@
"day": "يوم",
"days": "ايام",
"deduplicate_all": "إلغاء تكرار الكل",
"default_locale": "الإعدادات المحلية الافتراضية",
"default_locale_description": "تنسيق التواريخ والأرقام بناءً على الإعدادات المحلية للمتصفح",
"delete": "حذف",
"delete_action_confirmation_message": "هل انت متأكد من حذف هذا الملف؟ هذا سؤدي الى نقل الملف الى سلة مهملات الخادم وسيتم اشعارك ان كنت تريد حذفه على الجهاز",
"delete_action_prompt": "تم حذف {count}",
@@ -1389,11 +1384,9 @@
"library_page_sort_title": "عنوان الألبوم",
"licenses": "رُخَص",
"light": "المضيئ",
"light_theme": "التبديل إلى المظهر الفاتح",
"like": "اعجاب",
"like_deleted": "تم حذف الإعجاب",
"link_motion_video": "رابط فيديو الحركة",
"link_to_docs": "لمزيد من المعلومات، يُرجى الرجوع إلى <link>الوثائق</link>.",
"link_to_oauth": "الربط مع OAuth",
"linked_oauth_account": "حساب مرتبط بـ OAuth",
"list": "قائمة",
@@ -2217,7 +2210,6 @@
"tag": "العلامة",
"tag_assets": "أصول العلامة",
"tag_created": "تم إنشاء العلامة: {tag}",
"tag_face": "علِّم الوجه",
"tag_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب مواضيع العلامات المنطقية",
"tag_not_found_question": "لا يمكن العثور على علامة؟ <link>قم بإنشاء علامة جديدة.</link>",
"tag_people": "علِّم الأشخاص",
@@ -2399,7 +2391,6 @@
"viewer_remove_from_stack": "حذف من الكومه أو المجموعة",
"viewer_stack_use_as_main_asset": "استخدم كأصل رئيسي",
"viewer_unstack": "فك الكومه",
"visibility": "إمكانية الرؤية",
"visibility_changed": "الرؤية تغيرت لـ {count, plural, one {شخص واحد} other {# عدة أشخاص}}",
"visual": "مرئي",
"visual_builder": "اداة نشاء مرئية",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Създаване на линк за споделяне",
"create_link_to_share_description": "Позволете на всеки, който има линк, да види избраната(ите) снимка(и)",
"create_new": "СЪЗДАЙ НОВ",
"create_new_face": "Създай ново лице",
"create_new_person": "Създаване на ново лице",
"create_new_person_hint": "Присвойте избраните файлове на нов човек",
"create_new_user": "Създаване на нов потребител",
"create_person": "Създай човек",
"create_person_subtitle": "Добави име към избраното лице за да създадеш и да сложиш етикет на новия човек",
"create_shared_album_page_share_add_assets": "ДОБАВИ ОБЕКТИ",
"create_shared_album_page_share_select_photos": "Избери снимки",
"create_shared_link": "Създай линк за споделяне",
@@ -2217,7 +2214,6 @@
"tag": "Таг",
"tag_assets": "Тагни елементи",
"tag_created": "Създаден етикет: {tag}",
"tag_face": "Отбележи лице",
"tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове",
"tag_not_found_question": "Не можете да намерите етикет? <link>Създайте нов етикет.</link>",
"tag_people": "Отбележи Хора",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Crear enllaç per compartir",
"create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades",
"create_new": "CREAR NOU",
"create_new_face": "Crea una nova cara",
"create_new_person": "Crea una nova persona",
"create_new_person_hint": "Assigna els elements seleccionats a una persona nova",
"create_new_user": "Crea un usuari nou",
"create_person": "Crea una persona",
"create_person_subtitle": "Afegeix un nom a la cara seleccionada per crear i etiquetar la nova persona",
"create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS",
"create_shared_album_page_share_select_photos": "Escull fotografies",
"create_shared_link": "Crea un enllaç compartit",
@@ -2217,7 +2214,6 @@
"tag": "Etiqueta",
"tag_assets": "Etiquetar actius",
"tag_created": "Etiqueta creada: {tag}",
"tag_face": "Etiqueta una cara",
"tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques",
"tag_not_found_question": "No trobeu una etiqueta? <link>Crear una nova etiqueta.</link>",
"tag_people": "Etiquetar personas",
+4 -8
View File
@@ -541,7 +541,7 @@
"app_settings": "App-Einstellungen",
"app_stores": "App Stores",
"app_update_available": "App Update verfügbar",
"appears_in": "Enthalten in",
"appears_in": "Erscheint in",
"apply_count": "Anwenden ({count, number})",
"archive": "Archiv",
"archive_action_prompt": "{count} zum Archiv hinzugefügt",
@@ -812,8 +812,8 @@
"confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelöscht. Bist du sicher, dass du fortfahren möchten?",
"confirm_new_pin_code": "Neuen PIN-Code bestätigen",
"confirm_password": "Passwort bestätigen",
"confirm_tag_face": "Wollen Sie dieses Gesicht mit {name} taggen?",
"confirm_tag_face_unnamed": "Möchten Sie dieses Gesicht taggen?",
"confirm_tag_face": "Wollen Sie dieses Gesicht mit {name} markieren?",
"confirm_tag_face_unnamed": "Möchten Sie dieses Gesicht markieren?",
"connected_device": "Verbundenes Gerät",
"connected_to": "Verbunden mit",
"contain": "Vollständig",
@@ -849,12 +849,9 @@
"create_link_to_share": "Link zum Teilen erstellen",
"create_link_to_share_description": "Lass jeden mit dem Link die ausgewählten Fotos sehen",
"create_new": "NEUES ERSTELLEN",
"create_new_face": "Neues Gesicht erstellen",
"create_new_person": "Neue Person anlegen",
"create_new_person_hint": "Ausgewählte Dateien einer neuen Person zuweisen",
"create_new_user": "Neuen Nutzer erstellen",
"create_person": "Person anlegen",
"create_person_subtitle": "Gib dem gewählten Gesicht einen Namen um die neue Person zu erstellen und zu taggen",
"create_shared_album_page_share_add_assets": "INHALTE HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen",
"create_shared_link": "Geteilten Link erstellen",
@@ -1038,7 +1035,7 @@
"error_loading_partners": "Fehler beim Laden der Partner: {error}",
"error_retrieving_asset_information": "Fehler beim Abruf der Dateiinformationen",
"error_saving_image": "Fehler: {error}",
"error_tag_face_bounding_box": "Fehler beim Taggen des Gesichts - Begrenzungen können nicht abgerufen werden",
"error_tag_face_bounding_box": "Fehler beim Markieren des Gesichts - Begrenzungen können nicht abgerufen werden",
"error_title": "Fehler - Etwas ist schief gelaufen",
"error_while_navigating": "Fehler beim Navigieren zur Datei",
"errors": {
@@ -2217,7 +2214,6 @@
"tag": "Tag",
"tag_assets": "Dateien taggen",
"tag_created": "Tag erstellt: {tag}",
"tag_face": "Gesicht taggen",
"tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen",
"tag_not_found_question": "Kein Tag vorhanden? <link>Erstelle einen neuen Tag.</link>",
"tag_people": "Personen taggen",
+1 -5
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Δημιουργία συνδέσμου για διαμοιρασμό",
"create_link_to_share_description": "Επιτρέψτε σε οποιονδήποτε έχει τον σύνδεσμο να δει τη/τις επιλεγμένη/ες φωτογραφία/ες",
"create_new": "ΔΗΜΙΟΥΡΓΙΑ ΝΕΟΥ",
"create_new_face": "Δημιουργία νέου προσώπου",
"create_new_person": "Δημιουργία νέου ατόμου",
"create_new_person": "Δημιουργία νέου προσώπου",
"create_new_person_hint": "Αντιστοίχιση των επιλεγμένων αρχείων σε ένα νέο πρόσωπο",
"create_new_user": "Δημιουργία νέου χρήστη",
"create_person": "Δημιουργία ατόμου",
"create_person_subtitle": "Προσθέστε ένα όνομα στο επιλεγμένο πρόσωπο για να δημιουργηθεί και να επισημανθεί το νέο άτομο",
"create_shared_album_page_share_add_assets": "ΠΡΟΣΘΗΚΗ ΣΤΟΙΧΕΙΩΝ",
"create_shared_album_page_share_select_photos": "Επιλέξτε Φωτογραφίες",
"create_shared_link": "Δημιουργία κοινόχρηστου συνδέσμου",
@@ -2217,7 +2214,6 @@
"tag": "Ετικέτα",
"tag_assets": "Ετικετοποίηση στοιχείων",
"tag_created": "Δημιουργήθηκε ετικέτα: {tag}",
"tag_face": "Επισήμανση προσώπου",
"tag_feature_description": "Περιήγηση σε φωτογραφίες και βίντεο που είναι οργανωμένα σύμφωνα με λογικά θέματα ετικετών",
"tag_not_found_question": "Δεν μπορείτε να βρείτε μια ετικέτα; <link>Δημιουργήστε μια νέα ετικέτα.</link>",
"tag_people": "Επισήμανση ατόμων",
+2 -6
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Crear enlace compartido",
"create_link_to_share_description": "Permitir que cualquier persona con el enlace vea la(s) foto(s) seleccionada(s)",
"create_new": "CREAR NUEVO",
"create_new_face": "Crear nueva cara",
"create_new_person": "Crear nueva persona",
"create_new_person_hint": "Asignar los recursos seleccionados a una nueva persona",
"create_new_user": "Crear nuevo usuario",
"create_person": "Crear persona",
"create_person_subtitle": "Añade un nombre a la cara seleccionada para crear y etiquetar a la nueva persona",
"create_shared_album_page_share_add_assets": "AÑADIR RECURSOS",
"create_shared_album_page_share_select_photos": "Seleccionar fotos",
"create_shared_link": "Crear un enlace compartido",
@@ -1025,7 +1022,7 @@
"enable_biometric_auth_description": "Introduce tu código PIN para habilitar la autentificación biométrica",
"enabled": "Habilitado",
"end_date": "Fecha final",
"enqueued": "Añadido a la cola",
"enqueued": "Agregado a la cola",
"enter_wifi_name": "Introduce el nombre Wi-Fi",
"enter_your_pin_code": "Introduce tu código PIN",
"enter_your_pin_code_subtitle": "Introduce tu código PIN para acceder a la carpeta protegida",
@@ -1088,7 +1085,7 @@
"unable_to_add_partners": "No se pueden añadir miembros",
"unable_to_add_remove_archive": "No se pudo {archived, select, true {eliminar el recurso del} other {añadir el recurso al}} archivo",
"unable_to_add_remove_favorites": "No se pudo {favorite, select, true {añadir el recuso a} other {eliminar el recurso de}} los favoritos",
"unable_to_archive_unarchive": "No se pudo {archived, select, true {añadir el elemento al} other {quitar el elemento del}} archivo",
"unable_to_archive_unarchive": "No se pudo {archived, select, true {agregar el elemento al} other {quitar el elemento del}} archivo",
"unable_to_change_album_user_role": "No se puede cambiar la función del usuario del álbum",
"unable_to_change_date": "No se puede cambiar la fecha",
"unable_to_change_description": "Imposible cambiar la descripción",
@@ -2217,7 +2214,6 @@
"tag": "Etiqueta",
"tag_assets": "Etiquetar recursos",
"tag_created": "Etiqueta creada: {tag}",
"tag_face": "Etiquetar cara",
"tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lógicas",
"tag_not_found_question": "¿No encuentra una etiqueta? <link>Crea una nueva etiqueta.</link>",
"tag_people": "Etiquetar personas",
+2 -14
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Luo linkki jaettavaksi",
"create_link_to_share_description": "Salli kaikkien linkin saaneiden nähdä valitut kuvat",
"create_new": "LUO UUSI",
"create_new_face": "Luo uudet kasvot",
"create_new_person": "Luo uusi henkilö",
"create_new_person_hint": "Määritä valitut mediat uudelle henkilölle",
"create_new_user": "Luo uusi käyttäjä",
"create_person": "Luo henkilö",
"create_person_subtitle": "Lisää nimi valituille kasvoille luodaksesi uudelle henkilölle tunnisteen",
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
"create_shared_album_page_share_select_photos": "Valitse kuvat",
"create_shared_link": "Luo jakolinkki",
@@ -869,7 +866,6 @@
"crop_aspect_ratio_fixed": "Kiinteä",
"crop_aspect_ratio_free": "Vapaa",
"crop_aspect_ratio_original": "Alkuperäinen",
"crop_aspect_ratio_square": "Neliö",
"curated_object_page_title": "Asiat",
"current_device": "Nykyinen laite",
"current_pin_code": "Nykyinen PIN-koodi",
@@ -884,7 +880,7 @@
"daily_title_text_date": "E, dd MMM",
"daily_title_text_date_year": "E, dd MMM, yyyy",
"dark": "Tumma",
"dark_theme": "Vaihda tummaan teemaan",
"dark_theme": "Vaihda tumma teema",
"date": "Päivämäärä",
"date_after": "Päivämäärän jälkeen",
"date_and_time": "Päivämäärä ja aika",
@@ -895,8 +891,6 @@
"day": "Päivä",
"days": "Päivää",
"deduplicate_all": "Poista kaikkien kaksoiskappaleet",
"default_locale": "Oletuskieli",
"default_locale_description": "Muotoile päivämäärät ja luvut selaimesi kieliasetusten mukaan",
"delete": "Poista",
"delete_action_confirmation_message": "Haluatko varmasti poistaa tämän aineiston? Tämä toiminto siirtää aineiston palvelimen roskakoriin ja kysyy, haluatko poistaa sen myös paikallisesti",
"delete_action_prompt": "{count} poistettu",
@@ -972,7 +966,7 @@
"downloading_media": "Median lataaminen",
"drop_files_to_upload": "Pudota tiedostot mihin tahansa ladataksesi ne",
"duplicates": "Kaksoiskappaleet",
"duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos mitkään) ovat kaksoiskappaleita.",
"duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos mitkään) ovat kaksoiskappaleita",
"duration": "Kesto",
"edit": "Muokkaa",
"edit_album": "Muokkaa albumia",
@@ -1009,8 +1003,6 @@
"editor_edits_applied_success": "Muutokset otettu käyttöön",
"editor_flip_horizontal": "Käännä vaakatasossa",
"editor_flip_vertical": "Käännä pystytasossa",
"editor_handle_corner": "{corner, select, top_left {Vasen yläkulma} top_right {Oikea yläkulma} bottom_left {Vasen alakulma} bottom_right {Oikea alakulma} other {A}} kulman kahva",
"editor_handle_edge": "{edge, select, top {Yläreuna} bottom {Alareuna} left {Vasen reuna} right {Oikea reuna} other {En}} reunan kahva",
"editor_orientation": "Suunta",
"editor_reset_all_changes": "Nollaa muutokset",
"editor_rotate_left": "Kierrä 90° vastapäivään",
@@ -1389,11 +1381,9 @@
"library_page_sort_title": "Albumin otsikko",
"licenses": "Lisenssit",
"light": "Vaalea",
"light_theme": "Vaihda vaaleaan teemaan",
"like": "Tykkää",
"like_deleted": "Tykkäys poistettu",
"link_motion_video": "Linkitä liikevideo",
"link_to_docs": "Lisätietoja löytyy <link>dokumentaatiosta</link>.",
"link_to_oauth": "Linkki OAuth",
"linked_oauth_account": "Linkitetty OAuth-tili",
"list": "Lista",
@@ -2217,7 +2207,6 @@
"tag": "Tunniste",
"tag_assets": "Lisää tunnisteita",
"tag_created": "Luotu tunniste: {tag}",
"tag_face": "Merkitse kasvot",
"tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tunnisteotsikoiden mukaan",
"tag_not_found_question": "Etkö löydä tunnistetta? <link>Luo uusi tunniste.</link>",
"tag_people": "Merkitse henkilö tunnisteella",
@@ -2399,7 +2388,6 @@
"viewer_remove_from_stack": "Poista pinosta",
"viewer_stack_use_as_main_asset": "Käytä pääkohteena",
"viewer_unstack": "Pura pino",
"visibility": "Näkyvyys",
"visibility_changed": "{count, plural, one {# henkilön} other {# henkilöiden}} näkyvyys vaihdettu",
"visual": "Visuaalinen",
"visual_builder": "Visuaalinen koostaja",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Créer un lien pour partager",
"create_link_to_share_description": "Permettre à n'importe qui ayant le lien de voir la(es) photo(s) sélectionnée(s)",
"create_new": "NOUVEAU",
"create_new_face": "Créer un nouveau visage",
"create_new_person": "Créer une nouvelle personne",
"create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne",
"create_new_user": "Créer un nouvel utilisateur",
"create_person": "Créer une personne",
"create_person_subtitle": "Ajouter un nom au visage sélectionné pour créer et étiqueter la nouvelle personne",
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
"create_shared_link": "Créer un lien partagé",
@@ -2217,7 +2214,6 @@
"tag": "Étiquette",
"tag_assets": "Étiqueter les médias",
"tag_created": "Étiquette créée: {tag}",
"tag_face": "Étiqueter le visage",
"tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques",
"tag_not_found_question": "Vous ne trouvez pas une étiquette? <link>Créer une nouvelle étiquette.</link>",
"tag_people": "Étiqueter les personnes",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Crear ligazón para compartir",
"create_link_to_share_description": "Permitir que calquera persoa coa ligazón vexa a(s) foto(s) seleccionada(s)",
"create_new": "CREAR NOVO",
"create_new_face": "Crear nova cara",
"create_new_person": "Crear nova persoa",
"create_new_person_hint": "Asignar activos seleccionados a unha nova persoa",
"create_new_user": "Crear novo usuario",
"create_person": "Crear persona",
"create_person_subtitle": "Engade un nome á cara seleccionada para crear e etiquetar á nova persona",
"create_shared_album_page_share_add_assets": "ENGADIR ACTIVOS",
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
"create_shared_link": "Crear ligazón compartida",
@@ -2217,7 +2214,6 @@
"tag": "Etiqueta",
"tag_assets": "Etiquetar activos",
"tag_created": "Etiqueta creada: {tag}",
"tag_face": "Etiquetar cara",
"tag_feature_description": "Navegar por fotos e vídeos agrupados por temas de etiquetas lóxicas",
"tag_not_found_question": "Non atopa unha etiqueta? <link>Crear unha nova etiqueta.</link>",
"tag_people": "Etiquetar Persoas",
-7
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Crea link da condividere",
"create_link_to_share_description": "Permetti a chiunque con il link di vedere le foto selezionate",
"create_new": "CREA NUOVO",
"create_new_face": "Crea nuova faccia",
"create_new_person": "Crea nuova persona",
"create_new_person_hint": "Assegna le risorse selezionate a una nuova persona",
"create_new_user": "Crea nuovo utente",
"create_person": "Crea persona",
"create_person_subtitle": "Aggiungi un nome alla faccia selezionata per creare e taggare la nuova persona",
"create_shared_album_page_share_add_assets": "AGGIUNGI RISORSE",
"create_shared_album_page_share_select_photos": "Seleziona foto",
"create_shared_link": "Crea link condiviso",
@@ -895,8 +892,6 @@
"day": "Giorno",
"days": "Giorni",
"deduplicate_all": "Elimina tutti i doppioni",
"default_locale": "Predefinito Locale",
"default_locale_description": "Formatta le date e i numeri sulla base del tuo browser locale",
"delete": "Elimina",
"delete_action_confirmation_message": "Vuoi davvero eliminare questa risorsa? Questa azione sposterà la risorsa nel cestino del server e ti chiederà se desideri eliminarla dal dispositivo",
"delete_action_prompt": "{count} elementi eliminati",
@@ -1393,7 +1388,6 @@
"like": "Mi piace",
"like_deleted": "Mi piace rimosso",
"link_motion_video": "Collega video in movimento",
"link_to_docs": "Per maggiori informazioni, riferirsi al <link>documentazione</link>.",
"link_to_oauth": "Collegamento a OAuth",
"linked_oauth_account": "Account OAuth collegato",
"list": "Lista",
@@ -2217,7 +2211,6 @@
"tag": "Tag",
"tag_assets": "Tagga risorse",
"tag_created": "Tag creato: {tag}",
"tag_face": "Tagga la faccia",
"tag_feature_description": "Navigazione foto e video raggruppati per argomenti tag logici",
"tag_not_found_question": "Non riesci a trovare un tag? <link>Creane uno nuovo.</link>",
"tag_people": "Tagga persone",
+24 -36
View File
@@ -798,7 +798,7 @@
"command_palette_to_close": "닫기",
"command_palette_to_navigate": "들어가기",
"command_palette_to_select": "선택하기",
"command_palette_to_show_all": "모두 보기",
"command_palette_to_show_all": "다 보여주기",
"comment_deleted": "댓글이 삭제되었습니다.",
"comment_options": "댓글 옵션",
"comments_and_likes": "댓글 및 좋아요",
@@ -849,12 +849,9 @@
"create_link_to_share": "공유 링크 생성",
"create_link_to_share_description": "링크가 있는 경우 누구나 선택한 사진을 볼 수 있습니다.",
"create_new": "새로 만들기",
"create_new_face": "새 얼굴 생성",
"create_new_person": "인물 생성",
"create_new_person_hint": "선택한 항목의 인물을 새 인물로 변경",
"create_new_user": "새 사용자 생성",
"create_person": "인물 생성",
"create_person_subtitle": "선택한 얼굴에 이름을 추가해 신규 인물을 생성하고 태그 지정",
"create_shared_album_page_share_add_assets": "항목 추가",
"create_shared_album_page_share_select_photos": "사진 선택",
"create_shared_link": "공유 링크 생성",
@@ -869,7 +866,6 @@
"crop_aspect_ratio_fixed": "고정",
"crop_aspect_ratio_free": "직접 조절",
"crop_aspect_ratio_original": "원본",
"crop_aspect_ratio_square": "정사각형",
"curated_object_page_title": "사물",
"current_device": "현재 기기",
"current_pin_code": "현재 PIN 코드",
@@ -884,7 +880,7 @@
"daily_title_text_date": "M월 d일 EEEE",
"daily_title_text_date_year": "yyyy년 M월 d일 EEEE",
"dark": "다크",
"dark_theme": "다크 테마 전환",
"dark_theme": "다크 테마 토글",
"date": "날짜",
"date_after": "다음 날짜 이후",
"date_and_time": "날짜 및 시간",
@@ -895,8 +891,6 @@
"day": "일",
"days": "일",
"deduplicate_all": "모두 삭제",
"default_locale": "기본 로케일",
"default_locale_description": "브라우저 로케일 설정에 따라 날짜 및 숫자 형식을 지정합니다",
"delete": "삭제",
"delete_action_confirmation_message": "이 항목을 삭제하시겠습니까? 서버에서는 항목을 휴지통으로 이동시키며, 로컬에서도 삭제할 것인지 확인 메시지가 표시됩니다.",
"delete_action_prompt": "{count}개 항목 삭제됨",
@@ -1009,8 +1003,6 @@
"editor_edits_applied_success": "편집이 적용되었습니다.",
"editor_flip_horizontal": "좌우반전",
"editor_flip_vertical": "상하반전",
"editor_handle_corner": "{corner, select, top_left {좌상단} top_right {우상단} bottom_left {좌하단} bottom_right {우하단} other {A}} 코너 핸들",
"editor_handle_edge": "{edge, select, top {위} bottom {아래} left {왼쪽} right {오른쪽} other {An}} 모서리 핸들",
"editor_orientation": "방향",
"editor_reset_all_changes": "편집내용 초기화",
"editor_rotate_left": "반시계 방향으로 90° 회전",
@@ -1069,26 +1061,26 @@
"failed_to_load_assets": "항목 로드 실패",
"failed_to_load_notifications": "알림 로드 실패",
"failed_to_load_people": "인물 로드 실패",
"failed_to_remove_product_key": "제품 키 제거에 실패했습니다.",
"failed_to_remove_product_key": "제품 키 제거에 실패",
"failed_to_reset_pin_code": "PIN 코드 초기화 실패",
"failed_to_stack_assets": "항목 스택에 실패했습니다.",
"failed_to_unstack_assets": "항목 스택 풀기에 실패했습니다.",
"failed_to_stack_assets": "항목 스택에 실패",
"failed_to_unstack_assets": "항목 스택 풀기에 실패",
"failed_to_update_notification_status": "알림 상태 업데이트 실패",
"incorrect_email_or_password": "잘못된 이메일 또는 비밀번호",
"library_folder_already_exists": "가져올 경로가 이미 존재합니다.",
"page_not_found": "페이지를 찾을 수 없음",
"page_not_found": "페이지를 찾을 수 없음 :/",
"paths_validation_failed": "{paths, plural, one {경로 #개} other {경로 #개}}가 유효성 검사에 실패했습니다.",
"profile_picture_transparent_pixels": "프로필 사진에 투명 픽셀을 사용할 수 없습니다. 사진을 확대하거나 이동하세요.",
"quota_higher_than_disk_size": "할당량은 디스크 크기보다 작아야 합니다.",
"something_went_wrong": "문제가 발생했습니다.",
"unable_to_add_album_users": "앨범에 사용자를 추가할 수 없습니다.",
"unable_to_add_assets_to_shared_link": "항목을 공유 링크에 추가할 수 없습니다.",
"unable_to_add_comment": "댓글을 추가할 수 없습니다.",
"unable_to_add_exclusion_pattern": "제외 규칙을 추가할 수 없습니다.",
"unable_to_add_partners": "파트너를 추가할 수 없습니다.",
"unable_to_add_remove_archive": "{archived, select, true {보관함에서 항목을 제거할} other {보관함으로 항목을 이동할}} 수 없습니다.",
"unable_to_add_remove_favorites": "즐겨찾기에 항목을 {favorite, select, true {추가} other {제거}}할 수 없습니다",
"unable_to_archive_unarchive": "항목을 {archived, select, true {보관} other {보관 해제}}할 수 없습니다",
"unable_to_add_album_users": "앨범에 사용자를 추가할 수 없",
"unable_to_add_assets_to_shared_link": "항목을 공유 링크에 추가할 수 없",
"unable_to_add_comment": "댓글을 추가할 수 없",
"unable_to_add_exclusion_pattern": "제외 규칙을 추가할 수 없",
"unable_to_add_partners": "파트너를 추가할 수 없",
"unable_to_add_remove_archive": "{archived, select, true {보관함에서 항목을 제거할} other {보관함으로 항목을 이동할}} 수 없",
"unable_to_add_remove_favorites": "즐겨찾기에 항목을 {favorite, select, true {추가} other {제거}}할 수 없",
"unable_to_archive_unarchive": "항목을 {archived, select, true {보관} other {보관 해제}}할 수 없",
"unable_to_change_album_user_role": "앨범 사용자의 역할을 변경할 수 없습니다.",
"unable_to_change_date": "날짜를 변경할 수 없습니다.",
"unable_to_change_description": "설명을 변경할 수 없습니다.",
@@ -1134,10 +1126,10 @@
"unable_to_remove_library": "라이브러리를 제거할 수 없습니다.",
"unable_to_remove_partner": "파트너를 제거할 수 없습니다.",
"unable_to_remove_reaction": "반응을 제거할 수 없습니다.",
"unable_to_reset_password": "비밀번호를 초기화할 수 없습니다.",
"unable_to_reset_password": "비밀번호를 초기화할 수 없",
"unable_to_reset_pin_code": "PIN 코드를 초기화할 수 없음",
"unable_to_resolve_duplicate": "비슷한 항목을 처리할 수 없음",
"unable_to_restore_assets": "항목을 복원할 수 없습니다.",
"unable_to_restore_assets": "항목을 복원할 수 없",
"unable_to_restore_trash": "휴지통을 복원할 수 없습니다.",
"unable_to_restore_user": "사용자를 복원할 수 없습니다.",
"unable_to_save_album": "앨범을 저장할 수 없습니다.",
@@ -1150,7 +1142,7 @@
"unable_to_scan_library": "라이브러리를 스캔할 수 없습니다.",
"unable_to_set_feature_photo": "대표 사진을 설정할 수 없습니다.",
"unable_to_set_profile_picture": "프로필 사진을 설정할 수 없습니다.",
"unable_to_set_rating": "점을 정할 수 없습니다.",
"unable_to_set_rating": "점을 정할 수 없",
"unable_to_submit_job": "작업을 수행할 수 없습니다.",
"unable_to_trash_asset": "휴지통으로 이동할 수 없습니다.",
"unable_to_unlink_account": "계정 연결을 해제할 수 없습니다.",
@@ -1389,11 +1381,9 @@
"library_page_sort_title": "앨범명",
"licenses": "라이선스",
"light": "라이트",
"light_theme": "라이트 테마로 전환",
"like": "좋아요",
"like_deleted": "좋아요가 삭제되었습니다.",
"link_motion_video": "모션 비디오 링크",
"link_to_docs": "자세한 내용은 <link>문서</link>를 참조하십시오.",
"link_to_oauth": "OAuth에 연결",
"linked_oauth_account": "OAuth 계정이 연결되었습니다.",
"list": "목록",
@@ -1655,7 +1645,6 @@
"only_favorites": "즐겨찾기만",
"open": "열기",
"open_calendar": "캘린더 열기",
"open_in_browser": "브라우저에서 열기",
"open_in_map_view": "지도 보기에서 열기",
"open_in_openstreetmap": "OpenStreetMap에서 열기",
"open_the_search_filters": "검색 필터 열기",
@@ -1812,11 +1801,11 @@
"purchase_settings_server_activated": "서버 제품 키는 관리자가 제어합니다.",
"query_asset_id": "쿼리 항목 ID",
"queue_status": "전체 {total}, {count} 대기 중",
"rate_asset": "항목 점",
"rate_asset": "항목 점",
"rating": "별점",
"rating_clear": "점 초기화",
"rating_count": "{count, plural, =0 {점 없음} one {#점} other {#점}}",
"rating_description": "상세 정보 패널에 EXIF 별점 태그 표시",
"rating_clear": "점 초기화",
"rating_count": "{count, plural, =0 {점 없음} one {#점} other {#점}}",
"rating_description": "상세 정보 패널에 EXIF 등급 태그 표시",
"reaction_options": "반응 옵션",
"read_changelog": "변경 내역 보기",
"readonly_mode_disabled": "읽기 전용 모드 비활성화",
@@ -1953,7 +1942,7 @@
"search_filter_media_type_title": "미디어 종류 선택",
"search_filter_ocr": "OCR 검색",
"search_filter_people_title": "인물 선택",
"search_filter_star_rating": "점",
"search_filter_star_rating": "점",
"search_filter_tags_title": "태그 선택",
"search_for": "검색",
"search_for_existing_person": "존재하는 인물 검색",
@@ -1975,7 +1964,7 @@
"search_page_your_map": "나의 지도",
"search_people": "인물 검색",
"search_places": "장소 검색",
"search_rating": "별점으로 검색...",
"search_rating": "등급으로 검색...",
"search_result_page_new_search_hint": "새 검색",
"search_settings": "설정 검색",
"search_state": "지역 검색...",
@@ -2395,7 +2384,6 @@
"viewer_remove_from_stack": "스택에서 제거",
"viewer_stack_use_as_main_asset": "대표 항목으로 설정",
"viewer_unstack": "스택 풀기",
"visibility": "표시 설정",
"visibility_changed": "인물 {count, plural, one {#명} other {#명}}의 표시 여부가 변경됨",
"visual": "비주얼",
"visual_builder": "비주얼 빌더",
@@ -2426,7 +2414,7 @@
"yes": "네",
"you_dont_have_any_shared_links": "공유 링크가 없습니다.",
"your_wifi_name": "Wi-Fi 네트워크 이름",
"zero_to_clear_rating": "0을 눌러 항목 점 초기화",
"zero_to_clear_rating": "0을 눌러 항목 점 초기화",
"zoom_image": "이미지 확대",
"zoom_to_bounds": "화면에 맞춰 확대"
}
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Sukurti bendrinimo nuorodą",
"create_link_to_share_description": "Leisti bet kam su nuoroda matyti pažymėtą(-as) nuotrauką(-as)",
"create_new": "SUKURTI NAUJĄ",
"create_new_face": "Sukurti naują veidą",
"create_new_person": "Sukurti naują žmogų",
"create_new_person_hint": "Priskirti pasirinktus elementus naujam žmogui",
"create_new_user": "Sukurti naują varotoją",
"create_person": "Sukurti asmenį",
"create_person_subtitle": "Pridėkite vardą prie pasirinkto veido, kad sukurtumėte ir pažymėtumėte naują asmenį",
"create_shared_album_page_share_add_assets": "PRIDĖTI ELEMENTŲ",
"create_shared_album_page_share_select_photos": "Pažymėti nuotraukas",
"create_shared_link": "Sukurti dalijimosi nuorodą",
@@ -2217,7 +2214,6 @@
"tag": "Žyma",
"tag_assets": "Pažymėti",
"tag_created": "Sukurta žyma: {tag}",
"tag_face": "Pažymėti veidą",
"tag_feature_description": "Peržiūrėkite nuotraukas ir vaizdo įrašus sugrupuotus pagal sužymėtas temas",
"tag_not_found_question": "Nerandate žymos? <link>Sukurti naują žymą.</link>",
"tag_people": "Pažymėti Žmones",
+6 -10
View File
@@ -5,7 +5,7 @@
"acknowledge": "Bekreft",
"action": "Handling",
"action_common_update": "Oppdater",
"action_description": "Ett sett handlinger som skal utføres på de filtrerte mediefilene",
"action_description": "Ett sett med handlinger som skal utføres på de filtrerede objekter",
"actions": "Handlinger",
"active": "Aktiv",
"active_count": "Aktiv: {count}",
@@ -18,7 +18,7 @@
"add_a_title": "Legg til tittel",
"add_action": "Legg til hendelse",
"add_action_description": "Trykk for å legge til en hendelse å utføre",
"add_assets": "Legg til mediefiler",
"add_assets": "Legg til objekter",
"add_birthday": "Legg til bursdag",
"add_endpoint": "Legg til endepunkt",
"add_exclusion_pattern": "Legg til ekskluderingsmønster",
@@ -34,7 +34,7 @@
"add_to_album": "Legg til album",
"add_to_album_bottom_sheet_added": "Lagt til i {album}",
"add_to_album_bottom_sheet_already_exists": "Allerede i {album}",
"add_to_album_bottom_sheet_some_local_assets": "Noen lokale filer kunne ikke legges til i albumet",
"add_to_album_bottom_sheet_some_local_assets": "Noen lokale elementer kunne ikke legges til i albumet",
"add_to_album_toggle": "Avhuking for {album}",
"add_to_albums": "Legg til i album",
"add_to_albums_count": "Legg til i album ({count})",
@@ -50,8 +50,8 @@
"add_exclusion_pattern_description": "Legg til ekskluderingsmønstre. Globbing med *, ** og ? støttes. For å ignorere alle filer i en hvilken som helst mappe som heter \"Raw\", bruk \"**/Raw/**\". For å ignorere alle filer som slutter på \".tif\", bruk \"**/*.tif\". For å ignorere en absolutt filplassering, bruk \"/filsti/til/ignorer/**\".",
"admin_user": "Administrasjonsbruker",
"asset_offline_description": "Dette eksterne bibliotekselementet finnes ikke lenger på disk og har blitt flyttet til papirkurven. Hvis filen ble flyttet innad i biblioteket, se etter det tilsvarende elementet i tidslinjen din. For å gjenopprette elementet, vennligst sørg for at filstien under er tilgjengelig for Immich og skann biblioteket.",
"authentication_settings": "Godkjenings Instillinger",
"authentication_settings_description": "Administrer passord, OAuth, og andre innstillinger for autentiserings Instilinger",
"authentication_settings": "Godkjenninger",
"authentication_settings_description": "Administrer passord, OAuth, og andre innstillinger for autentisering",
"authentication_settings_disable_all": "Er du sikker på at du ønsker å deaktivere alle innloggingsmetoder? Innlogging vil bli fullstendig deaktivert.",
"authentication_settings_reenable": "For å aktivere på nytt, bruk en <link>Server Command</link>.",
"background_task_job": "Bakgrunnsjobber",
@@ -81,7 +81,7 @@
"cron_expression_description": "Still inn skanneintervallet med cron-formatet. For mer informasjon henvises til f.eks. <link>Crontab Guru</link>",
"cron_expression_presets": "Forhåndsinnstillinger for Cron-uttrykk",
"disable_login": "Deaktiver innlogging",
"duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av Smart Søk",
"duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage lignende bilder. Krever bruk av Smart Search",
"exclusion_pattern_description": "Ekskluderingsmønstre lar deg ignorere filer og mapper når du skanner biblioteket ditt. Dette er nyttig hvis du har mapper som inneholder filer du ikke vil importere, for eksempel RAW-filer.",
"export_config_as_json_description": "Last ned nåværende systemkonfigurasjon som en JSON fil",
"external_libraries_page_description": "Administrering for eksterne bibliotek",
@@ -849,12 +849,9 @@
"create_link_to_share": "Opprett delelink",
"create_link_to_share_description": "La alle med lenken se de(t) valgte bildet/bildene",
"create_new": "LAG NY",
"create_new_face": "Opprett nytt ansikt",
"create_new_person": "Opprett ny person",
"create_new_person_hint": "Tildel valgte eiendeler til en ny person",
"create_new_user": "Opprett ny bruker",
"create_person": "Opprett person",
"create_person_subtitle": "Gi det valgte ansiktet et navn for å opprette og tagge den nye personen",
"create_shared_album_page_share_add_assets": "LEGG TIL OBJEKTER",
"create_shared_album_page_share_select_photos": "Velg bilder",
"create_shared_link": "Opprett delt lenke",
@@ -2217,7 +2214,6 @@
"tag": "Tagg",
"tag_assets": "Merk ressurser",
"tag_created": "Lag merke: {tag}",
"tag_face": "Tagg ansikt",
"tag_feature_description": "Bla gjennom bilder og videoer gruppert etter logiske merke-emner",
"tag_not_found_question": "Finner du ikke en merke? <link>Opprett en nytt merke.</link>",
"tag_people": "Tag personer",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Gedeelde link maken",
"create_link_to_share_description": "Laat iedereen met de link de geselecteerde foto(s) zien",
"create_new": "MAAK NIEUW",
"create_new_face": "Nieuw gezicht aanmaken",
"create_new_person": "Nieuwe persoon aanmaken",
"create_new_person_hint": "Geselecteerde items toewijzen aan een nieuwe persoon",
"create_new_user": "Nieuwe gebruiker aanmaken",
"create_person": "Persoon aanmaken",
"create_person_subtitle": "Voeg een naam toe aan het geselecteerde gezicht om de nieuwe persoon aan te maken en te taggen",
"create_shared_album_page_share_add_assets": "ITEMS TOEVOEGEN",
"create_shared_album_page_share_select_photos": "Selecteer foto's",
"create_shared_link": "Gedeelde link maken",
@@ -2217,7 +2214,6 @@
"tag": "Tag",
"tag_assets": "Items taggen",
"tag_created": "Tag aangemaakt: {tag}",
"tag_face": "Gezicht labelen",
"tag_feature_description": "Bladeren door foto's en video's gegroepeerd op tags",
"tag_not_found_question": "Kun je een tag niet vinden? <link>Maak een nieuwe tag.</link>",
"tag_people": "Mensen taggen",
-18
View File
@@ -37,10 +37,8 @@
"add_to_album_bottom_sheet_some_local_assets": "Somme lokale eigedelar kunne ikkje leggjast til i album",
"add_to_albums": "Legg til i album",
"add_to_albums_count": "Legg til i album ({count})",
"add_to_bottom_bar": "Legg til i",
"add_to_shared_album": "Legg til i delt album",
"add_url": "Legg til URL",
"add_workflow_step": "Legg til steg i arbeidsflyt",
"added_to_archive": "Lagt til i arkiv",
"added_to_favorites": "Lagt til i favorittar",
"added_to_favorites_count": "La til {count, number} i favorittar",
@@ -73,7 +71,6 @@
"confirm_reprocess_all_faces": "Er du sikker på at du vil behandle alle ansikt på nytt? Det vil òg fjerne namngjevne personar.",
"confirm_user_password_reset": "Er du sikker at du vil tilbakestille passordet til {user}?",
"confirm_user_pin_code_reset": "Er du sikker på at du vil tilbakestille {user} sin PIN-kode?",
"copy_config_to_clipboard_description": "Kopier systemkonfigurasjonen som eit JSON-objekt til utklippstavla",
"create_job": "Lag jobb",
"cron_expression": "Cron uttrykk",
"cron_expression_description": "Set inn skanningsintervall med cron-formatet. For meir informasjon sjå t.d. <link>Crontab Guru</link>",
@@ -81,7 +78,6 @@
"disable_login": "Deaktiver innlogging",
"duplicate_detection_job_description": "Kjør maskinlæring på filer for å oppdage liknande bilete. Krev bruk av Smart Search",
"exclusion_pattern_description": "Utelatingsmønster let deg utelate filer og mapper når du skannar biblioteket ditt. Det er nyttig om du har mapper som inneheld filer du ikkje ynskjer å importere, til dømes RAW-filer.",
"export_config_as_json_description": "Last ned nåverande systemkonfigurasjon som ei JSON-fil",
"face_detection": "Ansiktssøk",
"face_detection_description": "Finn ansikt i bilete ved hjelp av maskinlæring. For videoar vert berre miniatyrbilete bruka. \"Alle\" søkjer (opp att) gjennom alle bilete. \"Tilbakestill\" fjernar all gjeldande ansiktsdata. \"Manglande\" legg filer som ikkje vert behandla til i køa for ansiktssøk. Oppdaga ansikt vert lagt i køa for ansiktsattkjenning, og kopla til eksisterande eller nye personar.",
"facial_recognition_job_description": "Koplar attkjende ansikt til personar. Det skjer fyrst når anskiktssøkjet er ferdig. \"Tilbakestill\" fjernar alle koplingar til personar, og tilbakestiller ansiktsgrupper. \"Manglande\" legg ansikt som ikkje er oppkopla til i køa.",
@@ -109,7 +105,6 @@
"image_thumbnail_description": "Lite miniatyrbilete med fjerna metadata, brukt når ein ser på grupper av bilete som hovudtidslinja",
"image_thumbnail_quality_description": "Kvalitet på miniatyrbilete frå 1-100. Høgare er betre, men gjev større filstorleik, og kan senkje appresposen.",
"image_thumbnail_title": "Innstillingar for miniatyrbilete",
"import_config_from_json_description": "Importer systemkonfigurasjon ved å laste opp ei JSON konfigurasjonsfil",
"job_concurrency": "{job} samstundes utføring",
"job_created": "Jobb laga",
"job_not_concurrency_safe": "Kan ikke trygt utføre jobben samstundes.",
@@ -117,30 +112,22 @@
"job_settings_description": "Handsam samstundes utføring av jobber",
"jobs_delayed": "{jobCount, plural, other {# forsinka}}",
"jobs_failed": "{jobCount, plural, other {# mislykkast}}",
"jobs_over_time": "Jobbar over tid",
"library_created": "Opprett bibliotek: {library}",
"library_deleted": "Bibliotek sletta",
"library_details": "Bibliotekdetaljar",
"library_folder_description": "Vel ei mappe å importere. Denne mappa, inkludert undermappar, vil bli skanna for biletar og videoar.",
"library_remove_exclusion_pattern_prompt": "Er du sikker på at du vil fjerne dette unntaksmønsteret?",
"library_scanning": "Regelbunden skanning",
"library_scanning_description": "Sett opp regelbunden skanning av biblioteket",
"library_scanning_enable_description": "Aktiver regelbunden skanning av biblioteket",
"library_settings": "Eksternt Bibliotek",
"library_settings_description": "Handsam eksterne biblioteksinnstillingar",
"library_tasks_description": "Utfør bibliotekstoppgåver",
"library_updated": "Oppdatert bibliotek",
"library_watching_enable_description": "Sjekk eksterne bibliotek for forandringar",
"library_watching_settings": "Biblioteksovervåking (EKSPERIMENTELL)",
"library_watching_settings_description": "Sjekk automatisk for forandringar",
"logging_enable_description": "Aktiver loggføring",
"logging_level_description": "Når aktivert, kva loggnivå å bruke.",
"logging_settings": "Logging",
"machine_learning_availability_checks": "Tilgjengelegheitssjekkar",
"machine_learning_availability_checks_description": "Automatiser oppdaging og prioritet av tilgjengelege maskinlærings-serverar",
"machine_learning_availability_checks_enabled": "Slå på tilgjengelegheitssjekkar",
"machine_learning_availability_checks_interval": "Sjekk intervall",
"machine_learning_availability_checks_timeout": "Tidsavbrot på forespørsel",
"machine_learning_availability_checks_timeout_description": "Utløpstid i millisekund for tilgjengelegheitssjekk",
"machine_learning_clip_model": "CLIP modell",
"machine_learning_clip_model_description": "Namnet på ein CLIP modell finst <link>her</link>. Merk at du må køyre 'Smart Søk'-jobben på nytt for alle bilete etter du har forandra modell.",
@@ -164,11 +151,6 @@
"machine_learning_min_detection_score_description": "Minimum tillitspoeng for at eit ansikt skal bli oppdaga, på ein skala frå 0 til 1. Lågare verdiar vil oppdage fleire ansikt, men kan føre til feilaktige treff.",
"machine_learning_min_recognized_faces": "Minimum gjenkjende ansikt",
"machine_learning_min_recognized_faces_description": "Minste tal på gjenkjende fjes for å opprette ein person. Aukar ein dette, vert ansiktsgjenkjenninga meir presis, på bekostning av auka sjanse for at ansikt ikkje vert tileigna ein person.",
"machine_learning_ocr": "OCR",
"machine_learning_ocr_description": "Bruk maskinlæring for å gjenkjenne tekst i bilete",
"machine_learning_ocr_enabled": "Slå på OCR",
"machine_learning_ocr_max_resolution": "Maksimal oppløysing",
"machine_learning_ocr_model": "OCR-modell",
"machine_learning_settings": "Innstillingar for maskinlæring",
"machine_learning_settings_description": "Administrer maskinlæringsfunksjonar og innstillingar",
"machine_learning_smart_search": "Smart Søk",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.7.0",
"version": "2.6.3",
"private": true,
"scripts": {
"format": "prettier --cache --check .",
+2 -2
View File
@@ -2224,12 +2224,12 @@
"tag_updated": "Uaktualniono etykietę: {tag}",
"tagged_assets": "Przypisano etykietę {count, plural, one {# zasobowi} other {# zasobom}}",
"tags": "Etykiety",
"tap_to_run_job": "Naciśnij, żeby uruchom zadanie",
"tap_to_run_job": "Uruchom zadanie",
"template": "Szablon",
"text_recognition": "Rozpoznawanie tekstu",
"theme": "Motyw",
"theme_selection": "Wybór motywu",
"theme_selection_description": "Automatycznie zmień motyw na jasny lub ciemny zależnie od ustawień systemu",
"theme_selection_description": "Automatycznie zmień motyw na jasny lub ciemny zależnie od ustawień przeglądarki",
"theme_setting_asset_list_storage_indicator_title": "Pokaż wskaźnik przechowywania na kafelkach zasobów",
"theme_setting_asset_list_tiles_per_row_title": "Liczba zasobów w wierszu ({count})",
"theme_setting_colorful_interface_subtitle": "Zastosuj kolor podstawowy do powierzchni tła.",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Criar link para partilhar",
"create_link_to_share_description": "Permitir a visualização desta(s) imagem(s) a qualquer pessoa com o link",
"create_new": "CRIAR NOVO",
"create_new_face": "Criar novo rosto",
"create_new_person": "Criar nova pessoa",
"create_new_person_hint": "Associe os ficheiros a uma nova pessoa",
"create_new_user": "Criar novo utilizador",
"create_person": "Criar pessoa",
"create_person_subtitle": "Adicione um nome ao rosto selecionado para criar e etiquetar a nova pessoa",
"create_shared_album_page_share_add_assets": "ADICIONAR FICHEIROS",
"create_shared_album_page_share_select_photos": "Selecionar Fotos",
"create_shared_link": "Criar link partilhado",
@@ -2217,7 +2214,6 @@
"tag": "Etiqueta",
"tag_assets": "Etiquetar ficheiros",
"tag_created": "Criada a etiqueta {tag}",
"tag_face": "Etiquetar rosto",
"tag_feature_description": "A mostrar fotos e videos agrupados por tópicos lógicos de etiquetas",
"tag_not_found_question": "Não consegue encontrar a etiqueta? <link>Crie uma nova etiqueta.</link>",
"tag_people": "Etiquetar Pessoas",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Создать ссылку общего доступа",
"create_link_to_share_description": "Разрешить всем, у кого есть ссылка, просматривать выбранные фотографии",
"create_new": "СОЗДАТЬ НОВЫЙ",
"create_new_face": "Создать новое лицо",
"create_new_person": "Добавить нового человека",
"create_new_person_hint": "Назначить выбранные объекты на нового человека",
"create_new_user": "Создать нового пользователя",
"create_person": "Создать человека",
"create_person_subtitle": "Укажите имя для создания нового человека",
"create_shared_album_page_share_add_assets": "ДОБАВИТЬ ОБЪЕКТЫ",
"create_shared_album_page_share_select_photos": "Выбрать фотографии",
"create_shared_link": "Создать общую ссылку",
@@ -2217,7 +2214,6 @@
"tag": "Тег",
"tag_assets": "Добавить теги",
"tag_created": "Тег {tag} создан",
"tag_face": "Отметить человека",
"tag_feature_description": "Просмотр фотографий и видео, сгруппированных по тегам",
"tag_not_found_question": "Не удается найти тег? <link>Создайте новый тег.</link>",
"tag_people": "Отметить человека",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Ustvari povezavo za skupno rabo",
"create_link_to_share_description": "Omogoči vsem s povezavo ogled izbranih fotografij",
"create_new": "USTVARI NOVEGA",
"create_new_face": "Ustvari nov obraz",
"create_new_person": "Ustvari novo osebo",
"create_new_person_hint": "Dodeli izbrana sredstva novi osebi",
"create_new_user": "Ustvari novega uporabnika",
"create_person": "Ustvari osebo",
"create_person_subtitle": "Dodajte ime izbranemu obrazu, da ustvarite in označite novo osebo",
"create_shared_album_page_share_add_assets": "DODAJ SREDSTVA",
"create_shared_album_page_share_select_photos": "Izberi fotografije",
"create_shared_link": "Ustvari deljeno povezavo",
@@ -2217,7 +2214,6 @@
"tag": "Oznaka",
"tag_assets": "Označi sredstva",
"tag_created": "Ustvarjena oznaka: {tag}",
"tag_face": "Označi obraz",
"tag_feature_description": "Brskanje po fotografijah in videoposnetkih, razvrščenih po temah logičnih oznak",
"tag_not_found_question": "Ne najdete oznake? <link>Ustvarite novo oznako.</link>",
"tag_people": "Označi osebe",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Skapa länk att dela",
"create_link_to_share_description": "Låt alla med länken se de valda fotona",
"create_new": "SKAPA NY",
"create_new_face": "Skapa nytt ansikte",
"create_new_person": "Skapa ny person",
"create_new_person_hint": "Tilldela valda objekt till en ny person",
"create_new_user": "Skapa en ny användare",
"create_person": "Skapa person",
"create_person_subtitle": "Lägg till ett namn till det valda ansiktet för att skapa och tagga den nya personen",
"create_shared_album_page_share_add_assets": "LÄGG TILL OBJEKT",
"create_shared_album_page_share_select_photos": "Välj bilder",
"create_shared_link": "Skapa delad länk",
@@ -2217,7 +2214,6 @@
"tag": "Tagg",
"tag_assets": "Tagga objekt",
"tag_created": "Skapade tagg: {tag}",
"tag_face": "Tagga ansikte",
"tag_feature_description": "Bläddra bland foton och videor grupperade efter logiska taggar",
"tag_not_found_question": "Kan du inte hitta en tagg? <link>Skapa en ny tagg.</link>",
"tag_people": "Tagga Personer",
-3
View File
@@ -2123,12 +2123,9 @@
"sync_upload_album_setting_subtitle": "สร้างและอัปโหลดรูปภาพและวิดีโอของคุณไปยังอัลบั้มที่เลือกบน Immich",
"tag": "แท็ก",
"tag_created": "สร้างแท็ก: {tag}",
"tag_face": "แท็กใบหน้า",
"tag_feature_description": "ดูรูปถ่ายและวีดีโอที่สร้างกลุ่มตามหัวข้อแท็ก",
"tag_not_found_question": "ไม่สามารถหาแท็กได้ใช่หรือไม่?<link>สร้างแท็กใหม่</link>",
"tag_people": "แท็กผู้คน",
"tag_updated": "แท็กที่ถูกอัพเดต: {tag}",
"tagged_assets": "ที่ถูกแท็ก",
"tags": "แท็ก",
"tap_to_run_job": "แตะเพื่อรันงาน",
"template": "เท็มเพลต",
-4
View File
@@ -849,12 +849,9 @@
"create_link_to_share": "Paylaşmak için link oluştur",
"create_link_to_share_description": "Bağlantıya sahip olan herkesin seçilen fotoğrafları görmesine izin ver",
"create_new": "YENİ OLUŞTUR",
"create_new_face": "Yeni yüz oluştur",
"create_new_person": "Yeni kişi oluştur",
"create_new_person_hint": "Seçili öğeleri yeni bir kişiye atayın",
"create_new_user": "Yeni kullanıcı oluştur",
"create_person": "Kişi oluştur",
"create_person_subtitle": "Seçilen yüze bir isim ekleyerek yeni kişiyi oluşturun ve etiketleyin",
"create_shared_album_page_share_add_assets": "ÖĞELER EKLE",
"create_shared_album_page_share_select_photos": "Fotoğrafları Seç",
"create_shared_link": "Paylaşılan bağlantı oluştur",
@@ -2217,7 +2214,6 @@
"tag": "Etiket",
"tag_assets": "Öğeleri etiketle",
"tag_created": "Etiket oluşturuldu: {tag}",
"tag_face": "Yüzü etiketle",
"tag_feature_description": "Etiket temalarına göre gruplandırılmış fotoğraf ve videoları keşfedin",
"tag_not_found_question": "Etiket bulunamadı mı? <link>Yeni bir etiket oluşturun.</link>",
"tag_people": "İnsanları etiketle",
+3 -7
View File
@@ -40,7 +40,7 @@
"add_to_albums_count": "添加到相册 ({count})",
"add_to_bottom_bar": "添加到",
"add_to_shared_album": "添加到共享相册",
"add_upload_to_stack": "添加上传至堆",
"add_upload_to_stack": "添加上传至堆",
"add_url": "添加 URL",
"add_workflow_step": "添加工作流步骤",
"added_to_archive": "添加至存档",
@@ -622,7 +622,7 @@
"backup": "备份",
"backup_album_selection_page_albums_device": "设备上的相簿({count}",
"backup_album_selection_page_albums_tap": "单击包含,双击排除",
"backup_album_selection_page_assets_scatter": "资源可以分散在多个相册中。因此,在备份过程中,可以包含或排除某些相册。",
"backup_album_selection_page_assets_scatter": "选择包含或排除特定的相簿。(因为文件可能分散在多个相簿中)",
"backup_album_selection_page_select_albums": "选择相簿",
"backup_album_selection_page_selection_info": "选择信息",
"backup_album_selection_page_total_assets": "选中的照片或视频总数",
@@ -809,7 +809,7 @@
"confirm_admin_password": "确认管理员密码",
"confirm_delete_face": "确定要从此文件中删除 {name} 的面部信息吗?",
"confirm_delete_shared_link": "确定要删除此共享链接吗?",
"confirm_keep_this_delete_others": "堆中除此项目外的所有其他项目都将被删除。确定要继续吗?",
"confirm_keep_this_delete_others": "堆中除此项目外的所有其他项目都将被删除。确定要继续吗?",
"confirm_new_pin_code": "确认新 PIN 码",
"confirm_password": "确认密码",
"confirm_tag_face": "是否将此人脸标记为 {name}",
@@ -849,12 +849,9 @@
"create_link_to_share": "创建共享链接",
"create_link_to_share_description": "允许任何拥有链接的人查看所选照片",
"create_new": "新建",
"create_new_face": "创建新人脸",
"create_new_person": "创建新人物",
"create_new_person_hint": "将所选照片/视频分配给新人物",
"create_new_user": "新建用户",
"create_person": "创建人物",
"create_person_subtitle": "为所选人脸添加姓名,以创建并标记新人物",
"create_shared_album_page_share_add_assets": "添加照片/视频",
"create_shared_album_page_share_select_photos": "选择照片",
"create_shared_link": "创建共享链接",
@@ -2217,7 +2214,6 @@
"tag": "标签",
"tag_assets": "标记项目",
"tag_created": "已创建标签:{tag}",
"tag_face": "标记人脸",
"tag_feature_description": "按逻辑标签分组并浏览照片和视频",
"tag_not_found_question": "找不到标签吗?<link>创建新标签。</link>",
"tag_people": "命名人物",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "2.7.0"
version = "2.6.3"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"
+1 -1
View File
@@ -898,7 +898,7 @@ wheels = [
[[package]]
name = "immich-ml"
version = "2.7.0"
version = "2.6.3"
source = { editable = "." }
dependencies = [
{ name = "aiocache" },
+1 -1
View File
@@ -17,7 +17,7 @@ config_roots = [
node = "24.14.1"
flutter = "3.35.7"
pnpm = "10.32.1"
terragrunt = "0.99.5"
terragrunt = "0.99.4"
opentofu = "1.11.5"
java = "21.0.2"
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3042,
"android.injected.version.name" => "2.7.0",
"android.injected.version.code" => 3041,
"android.injected.version.name" => "2.6.3",
}
)
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')
+7 -29
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -16,7 +16,6 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01DD6982F7F43B40049AB63 /* ImageRequest.swift */; };
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
@@ -103,7 +102,6 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; };
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
@@ -139,23 +137,20 @@
);
target = F0B57D372DF764BD00DC5BCC /* WidgetExtension */;
};
FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Mutex.swift,
);
target = 97C146ED1CF9000F007C117D /* Runner */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
B231F52D2E93A44A00BC45D1 /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Core;
sourceTree = "<group>";
};
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
@@ -167,16 +162,10 @@
path = WidgetExtension;
sourceTree = "<group>";
};
FE1BB4562F8319560087DBF9 /* Utility */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
FE1BB4572F83196E0087DBF9 /* Exceptions for "Utility" folder in "Runner" target */,
);
path = Utility;
sourceTree = "<group>";
};
FEE084F22EC172080045228E /* Schemas */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Schemas;
sourceTree = "<group>";
};
@@ -284,7 +273,6 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
FE1BB4562F8319560087DBF9 /* Utility */,
FEE084F22EC172080045228E /* Schemas */,
B231F52D2E93A44A00BC45D1 /* Core */,
B25D37792E72CA15008B6CA7 /* Connectivity */,
@@ -339,7 +327,6 @@
FED3B1952E253E9B0030FD97 /* Images */ = {
isa = PBXGroup;
children = (
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */,
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */,
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */,
FE5499F52F11980E006016CB /* LocalImagesImpl.swift */,
@@ -571,14 +558,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";
@@ -607,14 +590,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";
@@ -629,7 +608,6 @@
files = (
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */,
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */,
FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */,
@@ -1,12 +1,7 @@
import Foundation
enum ImageProcessing {
static let queue = {
let q = OperationQueue()
q.name = "thumbnail.processing"
q.qualityOfService = .userInitiated
q.maxConcurrentOperationCount = ProcessInfo.processInfo.activeProcessorCount * 2
return q
}()
static let queue = DispatchQueue(label: "thumbnail.processing", qos: .userInitiated, attributes: .concurrent)
static let semaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
}
@@ -1,14 +0,0 @@
import Foundation
struct RequestRegistry<T: AnyObject & Sendable>: ~Copyable, Sendable {
private let requests = Mutex<[Int64: T]>([:])
func add(requestId: Int64, request: T) {
requests.withLock { $0[requestId] = request }
}
@discardableResult
func remove(requestId: Int64) -> T? {
requests.withLock { $0.removeValue(forKey: requestId) }
}
}
+52 -24
View File
@@ -4,18 +4,13 @@ import MobileCoreServices
import Photos
class LocalImageRequest {
weak var operation: Operation?
weak var workItem: DispatchWorkItem?
var isCancelled = false
let callback: (Result<[String: Int64]?, any Error>) -> Void
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
self.callback = callback
}
func cancel() {
isCancelled = true
operation?.cancel()
}
}
class LocalImageApiImpl: LocalImageApi {
@@ -36,7 +31,9 @@ class LocalImageApiImpl: LocalImageApi {
return requestOptions
}()
private static let registry = RequestRegistry<LocalImageRequest>()
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
private static var rgbaFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
@@ -45,6 +42,7 @@ class LocalImageApiImpl: LocalImageApi {
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
renderingIntent: .defaultIntent
)!
private static var requests = [Int64: LocalImageRequest]()
private static let assetCache = {
let assetCache = NSCache<NSString, PHAsset>()
assetCache.countLimit = 10000
@@ -52,7 +50,7 @@ class LocalImageApiImpl: LocalImageApi {
}()
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
ImageProcessing.queue.addOperation {
ImageProcessing.queue.async {
guard let data = Data(base64Encoded: thumbhash)
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
@@ -68,14 +66,23 @@ class LocalImageApiImpl: LocalImageApi {
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
let request = LocalImageRequest(callback: completion)
let operation = BlockOperation {
let item = DispatchWorkItem {
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
ImageProcessing.semaphore.wait()
defer {
ImageProcessing.semaphore.signal()
}
if request.isCancelled {
return completion(ImageProcessing.cancelledResult)
}
guard let asset = Self.requestAsset(assetId: assetId)
else {
Self.registry.remove(requestId: requestId)
Self.remove(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
}
@@ -100,11 +107,12 @@ class LocalImageApiImpl: LocalImageApi {
)
if request.isCancelled {
Self.remove(requestId: requestId)
return completion(ImageProcessing.cancelledResult)
}
guard let data = imageData else {
Self.registry.remove(requestId: requestId)
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
}
@@ -114,6 +122,7 @@ class LocalImageApiImpl: LocalImageApi {
if request.isCancelled {
free(pointer)
Self.remove(requestId: requestId)
return completion(ImageProcessing.cancelledResult)
}
@@ -121,7 +130,7 @@ class LocalImageApiImpl: LocalImageApi {
"pointer": Int64(Int(bitPattern: pointer)),
"length": Int64(length),
]))
Self.registry.remove(requestId: requestId)
Self.remove(requestId: requestId)
return
}
@@ -142,7 +151,7 @@ class LocalImageApiImpl: LocalImageApi {
guard let image = image,
let cgImage = image.cgImage else {
Self.registry.remove(requestId: requestId)
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
@@ -162,32 +171,51 @@ class LocalImageApiImpl: LocalImageApi {
"pointer": Int64(Int(bitPattern: buffer.data)),
"width": Int64(buffer.width),
"height": Int64(buffer.height),
"rowBytes": Int64(buffer.rowBytes),
"rowBytes": Int64(buffer.rowBytes)
]))
Self.registry.remove(requestId: requestId)
Self.remove(requestId: requestId)
} catch {
Self.registry.remove(requestId: requestId)
Self.remove(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
}
}
request.operation = operation
Self.registry.add(requestId: requestId, request: request)
ImageProcessing.queue.addOperation(operation)
request.workItem = item
Self.add(requestId: requestId, request: request)
ImageProcessing.queue.async(execute: item)
}
func cancelRequest(requestId: Int64) {
Self.registry.remove(requestId: requestId)?.cancel()
Self.cancel(requestId: requestId)
}
private static func add(requestId: Int64, request: LocalImageRequest) -> Void {
requestQueue.sync { requests[requestId] = request }
}
private static func remove(requestId: Int64) -> Void {
requestQueue.sync { requests[requestId] = nil }
}
private static func cancel(requestId: Int64) -> Void {
requestQueue.async {
guard let request = requests.removeValue(forKey: requestId) else { return }
request.isCancelled = true
guard let item = request.workItem else { return }
if item.isCancelled {
cancelQueue.async { request.callback(ImageProcessing.cancelledResult) }
}
}
}
private static func requestAsset(assetId: String) -> PHAsset? {
if let cached = assetCache.object(forKey: assetId as NSString) {
return cached
}
var asset: PHAsset?
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
if asset != nil { return asset }
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
else { return nil }
assetCache.setObject(asset, forKey: assetId as NSString)
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
return asset
}
}
+21 -11
View File
@@ -14,15 +14,11 @@ class RemoteImageRequest {
self.task = task
self.completion = completion
}
func cancel() {
isCancelled = true
task?.cancel()
}
}
class RemoteImageApiImpl: NSObject, RemoteImageApi {
private static let registry = RequestRegistry<RemoteImageRequest>()
private static var lock = os_unfair_lock()
private static var requests = [Int64: RemoteImageRequest]()
private static var rgbaFormat = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: 32,
@@ -47,15 +43,20 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
let request = RemoteImageRequest(id: requestId, task: task, completion: completion)
Self.registry.add(requestId: requestId, request: request)
os_unfair_lock_lock(&Self.lock)
Self.requests[requestId] = request
os_unfair_lock_unlock(&Self.lock)
task.resume()
}
private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
guard let request = registry.remove(requestId: requestId) else {
return
os_unfair_lock_lock(&Self.lock)
guard let request = requests[requestId] else {
return os_unfair_lock_unlock(&Self.lock)
}
requests[requestId] = nil
os_unfair_lock_unlock(&Self.lock)
if let error = error {
if request.isCancelled || (error as NSError).code == NSURLErrorCancelled {
@@ -72,7 +73,10 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil)))
}
ImageProcessing.queue.addOperation {
ImageProcessing.queue.async {
ImageProcessing.semaphore.wait()
defer { ImageProcessing.semaphore.signal() }
if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult)
}
@@ -126,7 +130,13 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
}
func cancelRequest(requestId: Int64) {
Self.registry.remove(requestId: requestId)?.cancel()
os_unfair_lock_lock(&Self.lock)
let request = Self.requests[requestId]
os_unfair_lock_unlock(&Self.lock)
guard let request = request else { return }
request.isCancelled = true
request.task?.cancel()
}
func clearCache(completion: @escaping (Result<Int64, any Error>) -> Void) {
+1 -1
View File
@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.7.0</string>
<string>2.6.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
-54
View File
@@ -1,54 +0,0 @@
import Darwin
// Can be replaced with std Mutex when the deployment target is iOS 18+
struct Mutex<Value: ~Copyable>: ~Copyable, @unchecked Sendable {
struct _Buffer: ~Copyable {
var lock: os_unfair_lock = .init()
var value: Value
init(value: consuming Value) {
self.value = value
}
deinit {}
}
let _buffer: UnsafeMutablePointer<_Buffer>
init(_ initialValue: consuming sending Value) {
_buffer = .allocate(capacity: 1)
_buffer.initialize(to: _Buffer(value: initialValue))
}
deinit {
_buffer.deinitialize(count: 1)
_buffer.deallocate()
}
@discardableResult
borrowing func withLock<Result: ~Copyable, E: Error>(
_ body: (inout sending Value) throws(E) -> sending Result
) throws(E) -> sending Result {
os_unfair_lock_lock(&_buffer.pointee.lock)
defer { os_unfair_lock_unlock(&_buffer.pointee.lock) }
return try body(&_buffer.pointee.value)
}
}
// Can be replaced with OSAllocatedUnfairLock when the deployment target is iOS 16+
typealias UnfairLock = Mutex<Void>
extension Mutex where Value == Void {
init() {
self.init(())
}
@discardableResult
borrowing func withLock<Result: ~Copyable, E: Error>(
_ body: () throws(E) -> sending Result
) throws(E) -> sending Result {
os_unfair_lock_lock(&_buffer.pointee.lock)
defer { os_unfair_lock_unlock(&_buffer.pointee.lock) }
return try body()
}
}
@@ -10,19 +10,20 @@ class TrashBottomBar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
color: context.themeData.canvasColor,
padding: const EdgeInsets.symmetric(vertical: 8),
child: const SafeArea(
top: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DeleteTrashActionButton(source: ActionSource.timeline),
RestoreTrashActionButton(source: ActionSource.timeline),
],
return SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 64,
child: Container(
color: context.themeData.canvasColor,
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DeleteTrashActionButton(source: ActionSource.timeline),
RestoreTrashActionButton(source: ActionSource.timeline),
],
),
),
),
),
+13 -32
View File
@@ -1,36 +1,17 @@
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
RestartableTimer useTimer(Duration duration, void Function() callback) {
return use(_TimerHook(duration: duration, callback: callback));
}
class _TimerHook extends Hook<RestartableTimer> {
final Duration duration;
final void Function() callback;
const _TimerHook({required this.duration, required this.callback});
@override
HookState<RestartableTimer, Hook<RestartableTimer>> createState() => _TimerHookState();
}
class _TimerHookState extends HookState<RestartableTimer, _TimerHook> {
late RestartableTimer timer;
@override
void initHook() {
super.initHook();
timer = RestartableTimer(hook.duration, hook.callback);
}
@override
RestartableTimer build(BuildContext context) {
return timer;
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
RestartableTimer useTimer(Duration duration, VoidCallback callback) {
final latest = useRef(callback);
latest.value = callback;
final timer = useMemoized(
() => RestartableTimer(duration, () => latest.value()),
[duration],
);
useEffect(() => timer.cancel, [timer]);
return timer;
}
@@ -1,6 +1,5 @@
import 'dart:math';
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/colors.dart';
@@ -8,63 +7,26 @@ import 'package:immich_mobile/models/cast/cast_manager_state.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart';
class VideoControls extends ConsumerStatefulWidget {
class VideoControls extends HookConsumerWidget {
final String videoPlayerName;
static const List<Shadow> _controlShadows = [Shadow(color: Colors.black87, blurRadius: 6, offset: Offset(0, 1))];
const VideoControls({super.key, required this.videoPlayerName});
@override
ConsumerState<VideoControls> createState() => _VideoControlsState();
}
class _VideoControlsState extends ConsumerState<VideoControls> {
late final RestartableTimer _hideTimer;
AutoDisposeStateNotifierProvider<VideoPlayerNotifier, VideoPlayerState> get _provider =>
videoPlayerProvider(widget.videoPlayerName);
@override
void initState() {
super.initState();
_hideTimer = RestartableTimer(const Duration(seconds: 5), _onHideTimer);
}
@override
void didUpdateWidget(covariant VideoControls oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.videoPlayerName != widget.videoPlayerName) {
_hideTimer.reset();
}
}
@override
void dispose() {
_hideTimer.cancel();
super.dispose();
}
void _onHideTimer() {
if (!mounted) return;
if (ref.read(_provider).status == VideoPlaybackStatus.playing) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
}
void _toggle(bool isCasting) {
void _toggle(WidgetRef ref, bool isCasting) {
if (isCasting) {
ref.read(castProvider.notifier).toggle();
return;
} else {
ref.read(videoPlayerProvider(videoPlayerName).notifier).toggle();
}
ref.read(_provider.notifier).toggle();
}
void _onSeek(bool isCasting, double value) {
void _onSeek(WidgetRef ref, bool isCasting, double value) {
final seekTo = Duration(microseconds: value.toInt());
if (isCasting) {
@@ -72,30 +34,38 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
return;
}
ref.read(_provider.notifier).seekTo(seekTo);
ref.read(videoPlayerProvider(videoPlayerName).notifier).seekTo(seekTo);
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final provider = videoPlayerProvider(videoPlayerName);
final cast = ref.watch(castProvider);
final isCasting = cast.isCasting;
final (position, duration) = isCasting
? ref.watch(castProvider.select((c) => (c.currentTime, c.duration)))
: ref.watch(_provider.select((v) => (v.position, v.duration)));
: ref.watch(provider.select((v) => (v.position, v.duration)));
final videoStatus = ref.watch(_provider.select((v) => v.status));
final videoStatus = ref.watch(provider.select((v) => v.status));
final isPlaying = isCasting
? cast.castState == CastState.playing
: videoStatus == VideoPlaybackStatus.playing || videoStatus == VideoPlaybackStatus.buffering;
final isFinished = !isCasting && videoStatus == VideoPlaybackStatus.completed;
ref.listen(assetViewerProvider.select((v) => v.showingControls), (prev, showing) {
if (showing && prev != showing) _hideTimer.reset();
final hideTimer = useTimer(const Duration(seconds: 5), () {
if (!context.mounted) return;
if (ref.read(provider).status == VideoPlaybackStatus.playing) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
});
ref.listen(_provider.select((v) => v.status), (_, __) => _hideTimer.reset());
final notifier = ref.read(_provider.notifier);
ref.listen(assetViewerProvider.select((v) => v.showingControls), (prev, showing) {
if (showing && prev != showing) hideTimer.reset();
});
ref.listen(provider.select((v) => v.status), (_, __) => hideTimer.reset());
final notifier = ref.read(provider.notifier);
final isLoaded = duration != Duration.zero;
return Padding(
@@ -110,13 +80,9 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
padding: const EdgeInsets.all(12),
constraints: const BoxConstraints(),
icon: isFinished
? const Icon(Icons.replay, color: Colors.white, shadows: VideoControls._controlShadows)
: AnimatedPlayPause(
color: Colors.white,
playing: isPlaying,
shadows: VideoControls._controlShadows,
),
onPressed: () => _toggle(isCasting),
? const Icon(Icons.replay, color: Colors.white, shadows: _controlShadows)
: AnimatedPlayPause(color: Colors.white, playing: isPlaying, shadows: _controlShadows),
onPressed: () => _toggle(ref, isCasting),
),
const Spacer(),
Text(
@@ -125,7 +91,7 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
color: Colors.white,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
shadows: VideoControls._controlShadows,
shadows: _controlShadows,
),
),
const SizedBox(width: 12),
@@ -141,7 +107,7 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
padding: EdgeInsets.zero,
onChangeStart: (_) => notifier.hold(),
onChangeEnd: (_) => notifier.release(),
onChanged: isLoaded ? (value) => _onSeek(isCasting, value) : null,
onChanged: isLoaded ? (value) => _onSeek(ref, isCasting, value) : null,
),
],
),
+1 -1
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 2.7.0
- API version: 2.6.3
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
+3 -11
View File
@@ -15,36 +15,30 @@ class DatabaseBackupDto {
DatabaseBackupDto({
required this.filename,
required this.filesize,
required this.timezone,
});
String filename;
num filesize;
String timezone;
@override
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDto &&
other.filename == filename &&
other.filesize == filesize &&
other.timezone == timezone;
other.filesize == filesize;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(filename.hashCode) +
(filesize.hashCode) +
(timezone.hashCode);
(filesize.hashCode);
@override
String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize, timezone=$timezone]';
String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'filename'] = this.filename;
json[r'filesize'] = this.filesize;
json[r'timezone'] = this.timezone;
return json;
}
@@ -59,7 +53,6 @@ class DatabaseBackupDto {
return DatabaseBackupDto(
filename: mapValueOfType<String>(json, r'filename')!,
filesize: num.parse('${json[r'filesize']}'),
timezone: mapValueOfType<String>(json, r'timezone')!,
);
}
return null;
@@ -109,7 +102,6 @@ class DatabaseBackupDto {
static const requiredKeys = <String>{
'filename',
'filesize',
'timezone',
};
}
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 2.7.0+3042
version: 2.6.3+3041
environment:
sdk: '>=3.8.0 <4.0.0'
+2 -6
View File
@@ -15225,7 +15225,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "2.7.0",
"version": "2.6.3",
"contact": {}
},
"tags": [
@@ -17721,15 +17721,11 @@
},
"filesize": {
"type": "number"
},
"timezone": {
"type": "string"
}
},
"required": [
"filename",
"filesize",
"timezone"
"filesize"
],
"type": "object"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "2.7.0",
"version": "2.6.3",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",
+1 -2
View File
@@ -1,6 +1,6 @@
/**
* Immich
* 2.7.0
* 2.6.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -63,7 +63,6 @@ export type DatabaseBackupDeleteDto = {
export type DatabaseBackupDto = {
filename: string;
filesize: number;
timezone: string;
};
export type DatabaseBackupListResponseDto = {
backups: DatabaseBackupDto[];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
"version": "2.7.0",
"version": "2.6.3",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
+587 -649
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "2.7.0",
"version": "2.6.3",
"description": "",
"author": "",
"private": true,
@@ -149,7 +149,7 @@
"@types/react": "^19.0.0",
"@types/sanitize-html": "^2.13.0",
"@types/semver": "^7.5.8",
"@types/supertest": "^7.0.0",
"@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36",
"@types/validator": "^13.15.2",
"@vitest/coverage-v8": "^3.0.0",
-1
View File
@@ -4,7 +4,6 @@ import { IsString } from 'class-validator';
export class DatabaseBackupDto {
filename!: string;
filesize!: number;
timezone!: string;
}
export class DatabaseBackupListResponseDto {
@@ -75,10 +75,6 @@ export interface EnvData {
server: string;
};
versionCheck: {
url: string;
};
network: {
trustedProxies: string[];
};
@@ -324,10 +320,6 @@ const getEnv = (): EnvData => {
licensePublicKey: isProd ? productionKeys : stagingKeys,
versionCheck: {
url: isProd ? 'https://version.immich.cloud/version' : 'https://version.dev.immich.cloud/version',
},
network: {
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? ['linklocal', 'uniquelocal'],
},
@@ -66,8 +66,7 @@ export class ServerInfoRepository {
async getLatestRelease(): Promise<VersionResponse> {
try {
const { versionCheck } = this.configRepository.getEnv();
const response = await fetch(versionCheck.url);
const response = await fetch('https://version.immich.cloud/version');
if (!response.ok) {
throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`);
@@ -111,7 +111,6 @@ const validVideos = [
'.mpg',
'.mts',
'.mxf',
'.ts',
'.vob',
'.webm',
'.wmv',
@@ -283,7 +283,6 @@ export class DatabaseBackupService {
async listBackups(): Promise<DatabaseBackupListResponseDto> {
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const files = await this.storageRepository.readdir(backupsFolder);
const timezone = DateTime.local().zoneName;
const validFiles = files
.filter((fn) => isValidDatabaseBackupName(fn))
@@ -293,7 +292,7 @@ export class DatabaseBackupService {
const backups = await Promise.all(
validFiles.map(async (filename) => {
const stats = await this.storageRepository.stat(path.join(backupsFolder, filename));
return { filename, filesize: stats.size, timezone };
return { filename, filesize: stats.size };
}),
);
+11 -1
View File
@@ -2,8 +2,9 @@ import { DateTime } from 'luxon';
import { SemVer } from 'semver';
import { defaults } from 'src/config';
import { serverVersion } from 'src/constants';
import { JobName, JobStatus, SystemMetadataKey } from 'src/enum';
import { ImmichEnvironment, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
import { VersionService } from 'src/services/version.service';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@@ -72,6 +73,15 @@ describe(VersionService.name, () => {
});
describe('handVersionCheck', () => {
beforeEach(() => {
mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.Production }));
});
it('should not run in dev mode', async () => {
mocks.config.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.Development }));
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped);
});
it('should not run if the last check was < 60 minutes ago', async () => {
mocks.systemMetadata.get.mockResolvedValue({
checkedAt: DateTime.utc().minus({ minutes: 5 }).toISO(),
+6 -1
View File
@@ -4,7 +4,7 @@ import semver, { SemVer } from 'semver';
import { serverVersion } from 'src/constants';
import { OnEvent, OnJob } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { DatabaseLock, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
import { DatabaseLock, ImmichEnvironment, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { VersionCheckMetadata } from 'src/types';
@@ -71,6 +71,11 @@ export class VersionService extends BaseService {
try {
this.logger.debug('Running version check');
const { environment } = this.configRepository.getEnv();
if (environment === ImmichEnvironment.Development) {
return JobStatus.Skipped;
}
const { newVersionCheck } = await this.getConfig({ withCache: true });
if (!newVersionCheck.enabled) {
return JobStatus.Skipped;
-1
View File
@@ -83,7 +83,6 @@ describe('mimeTypes', () => {
{ mimetype: 'video/mp2t', extension: '.m2t' },
{ mimetype: 'video/mp2t', extension: '.m2ts' },
{ mimetype: 'video/mp2t', extension: '.mts' },
{ mimetype: 'video/mp2t', extension: '.ts' },
{ mimetype: 'video/mp4', extension: '.mp4' },
{ mimetype: 'video/mpeg', extension: '.mpe' },
{ mimetype: 'video/mpeg', extension: '.mpeg' },
-1
View File
@@ -114,7 +114,6 @@ const video: Record<string, string[]> = {
'.mpg': ['video/mpeg'],
'.mts': ['video/mp2t'],
'.mxf': ['application/mxf'],
'.ts': ['video/mp2t'],
'.vob': ['video/mpeg'],
'.webm': ['video/webm'],
'.wmv': ['video/x-ms-wmv'],
@@ -44,10 +44,6 @@ const envData: EnvData = {
server: 'server-public-key',
},
versionCheck: {
url: 'https://version.immich.cloud/version',
},
network: {
trustedProxies: [],
},
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "2.7.0",
"version": "2.6.3",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
@@ -89,7 +89,7 @@
"dotenv": "^17.0.0",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-compat": "^7.0.0",
"eslint-plugin-compat": "^6.0.2",
"eslint-plugin-svelte": "^3.12.4",
"eslint-plugin-unicorn": "^63.0.0",
"factory.ts": "^1.4.1",
@@ -99,7 +99,7 @@
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^7.0.0",
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.54.1",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3",
@@ -276,6 +276,7 @@
const handleStopSlideshow = async () => {
try {
if (document.fullscreenElement) {
document.body.style.cursor = '';
await document.exitFullscreen();
}
} catch (error) {
@@ -337,7 +338,7 @@
onAction?.(action);
};
let isFullScreen = $derived(!!fullscreenElement);
let isFullScreen = $derived(fullscreenElement !== null);
$effect(() => {
if (album && !album.isActivityEnabled && activityManager.commentCount === 0) {
@@ -85,7 +85,6 @@
});
onDestroy(() => {
setCursorStyle('');
if (unsubscribeRestart) {
unsubscribeRestart();
}
@@ -1,54 +0,0 @@
import { locale } from '$lib/stores/preferences.store';
import { renderWithTooltips } from '$tests/helpers';
import { screen } from '@testing-library/svelte';
import { DateTime } from 'luxon';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import MaintenanceBackupEntry from './MaintenanceBackupEntry.svelte';
vi.mock('$lib/services/database-backups.service', () => ({
getDatabaseBackupActions: () => ({
Download: { type: 'command', title: 'Download', onAction: vi.fn() },
Delete: { type: 'command', title: 'Delete', onAction: vi.fn() },
}),
handleRestoreDatabaseBackup: vi.fn(),
}));
describe('MaintenanceBackupEntry', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-03-24T12:00:00Z'));
locale.set('en');
});
afterEach(() => {
vi.useRealTimers();
});
it('renders relative backup time using the user timezone instead of UTC', () => {
const backupTimestamp = '20260324T110000';
const expectedRelativeTime = DateTime.fromFormat(backupTimestamp, "yyyyMMdd'T'HHmmss", {
zone: 'Asia/Tokyo',
})
.toLocal()
.toRelative({ locale: 'en' });
const utcRelativeTime = DateTime.fromFormat(backupTimestamp, "yyyyMMdd'T'HHmmss", {
zone: 'UTC',
})
.toLocal()
.toRelative({ locale: 'en' });
expect(expectedRelativeTime).toBeTruthy();
expect(expectedRelativeTime).not.toEqual(utcRelativeTime);
renderWithTooltips(MaintenanceBackupEntry, {
expectedVersion: '1.2.3',
filename: 'immich-db-backup-20260324T110000-v1.2.3-snapshot.sql.gz',
filesize: 1024,
timezone: 'Asia/Tokyo',
});
expect(screen.getByText(expectedRelativeTime!)).toBeInTheDocument();
});
});
@@ -13,17 +13,16 @@
filename: string;
filesize: number;
expectedVersion: string;
timezone?: string;
};
const { filename, filesize, expectedVersion, timezone }: Props = $props();
const { filename, filesize, expectedVersion }: Props = $props();
const filesizeText = $derived(getBytesWithUnit(filesize, 1));
const backupDateTime = $derived.by(() => {
const dateMatch = filename.match(/\d+T\d+/);
if (dateMatch) {
return DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: timezone }).toLocal();
return DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: 'utc' }).toLocal();
}
return null;
});
@@ -51,13 +51,12 @@
const unknownDateKey = $t('unknown_date');
for (const backup of backups) {
const timezone = backup.timezone;
const dateMatch = backup.filename.match(/\d+T\d+/);
let dateKey: string;
let dt: DateTime;
if (dateMatch) {
dt = DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: timezone });
dt = DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: 'utc' });
dateKey = dt.toFormat('LLLL d, yyyy');
} else {
dt = DateTime.fromMillis(0);
@@ -129,7 +128,6 @@
filename={backup.filename}
filesize={backup.filesize}
expectedVersion={props.expectedVersion}
timezone={backup.timezone}
/>
{/each}
</Stack>
@@ -161,30 +161,6 @@ describe('CancellableTask', () => {
expect(task.executed).toBe(true);
});
it('should return CANCELED when concurrent caller is waiting and task is canceled', async () => {
const task = new CancellableTask();
let resolveTask: () => void;
const taskPromise = new Promise<void>((resolve) => {
resolveTask = resolve;
});
const taskFn = async (signal: AbortSignal) => {
await taskPromise;
if (signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
};
const promise1 = task.execute(taskFn, true);
const promise2 = task.execute(taskFn, true);
task.cancel();
resolveTask!();
const [result1, result2] = await Promise.all([promise1, promise2]);
expect(result1).toBe('CANCELED');
expect(result2).toBe('CANCELED');
});
it('should not cancel if task is already executed', async () => {
const task = new CancellableTask();
const taskFn = vi.fn(async () => {});
+3 -9
View File
@@ -64,12 +64,8 @@ export class CancellableTask {
if (this.cancellable && !cancellable) {
this.cancellable = cancellable;
}
try {
await this.complete;
return 'WAITED';
} catch {
return 'CANCELED';
}
await this.complete;
return 'WAITED';
}
this.cancellable = cancellable;
const cancelToken = (this.cancelToken = new AbortController());
@@ -90,9 +86,7 @@ export class CancellableTask {
this.#transitionToErrored(error);
return 'ERRORED';
} finally {
if (this.cancelToken === cancelToken) {
this.cancelToken = null;
}
this.cancelToken = null;
}
}
-1
View File
@@ -1,7 +1,6 @@
# Allow social media access og Tags
User-agent: *
Allow: /share/
Allow: /s/
Allow: /api/assets/
# https://www.robotstxt.org/robotstxt.html