Compare commits

..

2 Commits

Author SHA1 Message Date
Alex
99a8740f1b only migrate for images 2026-01-27 22:37:12 -06:00
Alex
a781c78caf fix: width and height migration issue 2026-01-27 21:48:19 -06:00
69 changed files with 279 additions and 777 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.5.2",
"version": "2.5.1",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -1,7 +1,7 @@
[
{
"label": "v2.5.2",
"url": "https://docs.v2.5.2.archive.immich.app"
"label": "v2.5.1",
"url": "https://docs.v2.5.1.archive.immich.app"
},
{
"label": "v2.4.1",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.5.2",
"version": "2.5.1",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -1,116 +0,0 @@
import { faker } from '@faker-js/faker';
import { expect, test } from '@playwright/test';
import {
Changes,
createDefaultTimelineConfig,
generateTimelineData,
TimelineAssetConfig,
TimelineData,
} from 'src/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
const buildSearchUrl = (assetId: string) => {
const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' }));
return `/search/photos/${assetId}?query=${searchQuery}`;
};
test.describe.configure({ mode: 'parallel' });
test.describe('search gallery-viewer', () => {
let adminUserId: string;
let timelineRestData: TimelineData;
const assets: TimelineAssetConfig[] = [];
const testContext = new TimelineTestContext();
const changes: Changes = {
albumAdditions: [],
assetDeletions: [],
assetArchivals: [],
assetFavorites: [],
};
test.beforeAll(async () => {
adminUserId = faker.string.uuid();
testContext.adminId = adminUserId;
timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId });
for (const timeBucket of timelineRestData.buckets.values()) {
assets.push(...timeBucket);
}
});
test.beforeEach(async ({ context }) => {
await setupBaseMockApiRoutes(context, adminUserId);
await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext);
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));
return route.fulfill({
status: 200,
contentType: 'application/json',
json: {
albums: { total: 0, count: 0, items: [], facets: [] },
assets: {
total: searchAssets.length,
count: searchAssets.length,
items: searchAssets,
facets: [],
nextPage: null,
},
},
});
}
await route.fallback();
});
});
test.afterEach(() => {
testContext.slowBucket = false;
changes.albumAdditions = [];
changes.assetDeletions = [];
changes.assetArchivals = [];
changes.assetFavorites = [];
});
test.describe('/search/photos/:id', () => {
test('Deleting a photo advances to the next photo', async ({ page }) => {
const asset = assets[0];
await page.goto(buildSearchUrl(asset.id));
await assetViewerUtils.waitForViewerLoad(page, asset);
await page.getByLabel('Delete').click();
await assetViewerUtils.waitForViewerLoad(page, assets[1]);
});
test('Deleting two photos in a row advances to the next photo each time', async ({ page }) => {
const asset = assets[0];
await page.goto(buildSearchUrl(asset.id));
await assetViewerUtils.waitForViewerLoad(page, asset);
await page.getByLabel('Delete').click();
await assetViewerUtils.waitForViewerLoad(page, assets[1]);
await page.getByLabel('Delete').click();
await assetViewerUtils.waitForViewerLoad(page, assets[2]);
});
test('Navigating backward then deleting advances to the next photo', async ({ page }) => {
const asset = assets[1];
await page.goto(buildSearchUrl(asset.id));
await assetViewerUtils.waitForViewerLoad(page, asset);
await page.getByLabel('View previous asset').click();
await assetViewerUtils.waitForViewerLoad(page, assets[0]);
await page.getByLabel('View next asset').click();
await assetViewerUtils.waitForViewerLoad(page, asset);
await page.getByLabel('Delete').click();
await assetViewerUtils.waitForViewerLoad(page, assets[2]);
});
test('Deleting the last photo advances to the previous photo', async ({ page }) => {
const lastAsset = assets[4];
await page.goto(buildSearchUrl(lastAsset.id));
await assetViewerUtils.waitForViewerLoad(page, lastAsset);
await expect(page.getByLabel('View next asset')).toHaveCount(0);
await page.getByLabel('Delete').click();
await assetViewerUtils.waitForViewerLoad(page, assets[3]);
await expect(page.getByLabel('View previous asset')).toBeVisible();
});
});
});

View File

@@ -526,8 +526,8 @@
"allowed": "Erlaubt",
"alt_text_qr_code": "QR-Code Bild",
"always_keep": "Immer behalten",
"always_keep_photos_hint": "Speicherfreigabe wird alle Fotos auf dem Gerät behalten.",
"always_keep_videos_hint": "Speicherfreigabe wird alle Videos auf dem Gerät behalten.",
"always_keep_photos_hint": "Speicher freigeben wird alle Fotos auf dem Gerät behalten",
"always_keep_videos_hint": "Speicher freigeben wird alle Videos auf dem Gerät behalten",
"anti_clockwise": "Gegen den Uhrzeigersinn",
"api_key": "API-Schlüssel",
"api_key_description": "Dieser Wert wird nur einmal angezeigt. Bitte kopiere ihn, bevor du das Fenster schließt.",
@@ -537,7 +537,7 @@
"app_bar_signout_dialog_content": "Bist du dir sicher, dass du dich abmelden möchtest?",
"app_bar_signout_dialog_ok": "Ja",
"app_bar_signout_dialog_title": "Abmelden",
"app_download_links": "App Download-Links",
"app_download_links": "App Download Links",
"app_settings": "App-Einstellungen",
"app_stores": "App Stores",
"app_update_available": "App Update verfügbar",
@@ -572,9 +572,6 @@
"asset_list_layout_sub_title": "Layout",
"asset_list_settings_subtitle": "Einstellungen für das Fotogitter-Layout",
"asset_list_settings_title": "Fotogitter",
"asset_not_found_on_device_android": "Datei auf Gerät nicht gefunden",
"asset_not_found_on_device_ios": "Datei auf Gerät nicht gefunden. Wenn Du iCloud verwendest, kann die Datei möglicherweise nicht auffindbar sein aufgrund schlechter Dateispeicherung von iCloud",
"asset_not_found_on_icloud": "Datei in iCloud nicht gefunden. Die Datei kann möglicherweise nicht auffindbar sein aufgrund schlechter Dateispeicherung in iCloud",
"asset_offline": "Datei offline",
"asset_offline_description": "Diese externe Datei ist nicht mehr auf dem Datenträger vorhanden. Bitte wende dich an deinen Immich-Administrator, um Hilfe zu erhalten.",
"asset_restored_successfully": "Datei erfolgreich wiederhergestellt",
@@ -610,14 +607,14 @@
"assets_were_part_of_album_count": "{count, plural, one {# Datei ist} other {# Dateien sind}} bereits im Album vorhanden",
"assets_were_part_of_albums_count": "{count, plural, one {Datei war} other {Dateien waren}} bereits in den Alben",
"authorized_devices": "Verwendete Geräte",
"automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal über ein bestimmtes WiFi, wenn es verfügbar ist, und verwenden Sie andere Verbindungsmöglichkeiten",
"automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal über ein bestimmtes WLAN, wenn es verfügbar ist, und verwenden Sie andere Verbindungsmöglichkeiten anderswo",
"automatic_endpoint_switching_title": "Automatische URL-Umschaltung",
"autoplay_slideshow": "Automatische Diashow",
"back": "Zurück",
"back_close_deselect": "Zurück, Schließen oder Abwählen",
"background_backup_running_error": "Sicherung läuft im Hintergrund. Manuelle Sicherung kann nicht gestartet werden",
"background_location_permission": "Hintergrund Standortfreigabe",
"background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu können, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WiFi-Netzwerks ermitteln kann",
"background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu können, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann",
"background_options": "Hintergrund Optionen",
"backup": "Sicherung",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({count})",
@@ -652,7 +649,7 @@
"backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert",
"backup_controller_page_background_turn_off": "Hintergrundservice ausschalten",
"backup_controller_page_background_turn_on": "Hintergrundservice einschalten",
"backup_controller_page_background_wifi": "Nur im WiFi",
"backup_controller_page_background_wifi": "Nur im WLAN",
"backup_controller_page_backup": "Sicherung",
"backup_controller_page_backup_selected": "Ausgewählt: ",
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
@@ -754,7 +751,7 @@
"charging_requirement_mobile_backup": "Backup im Hintergrund erfordert Aufladen des Geräts",
"check_corrupt_asset_backup": "Auf beschädigte Asset-Backups überprüfen",
"check_corrupt_asset_backup_button": "Überprüfung durchführen",
"check_corrupt_asset_backup_description": "Führe diese Prüfung nur mit aktivierten WiFi durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.",
"check_corrupt_asset_backup_description": "Führe diese Prüfung nur mit aktivierten WLAN durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.",
"check_logs": "Logs prüfen",
"checksum": "Prüfsumme",
"choose_matching_people_to_merge": "Wähle passende Personen zum Zusammenführen",
@@ -991,12 +988,12 @@
"edit_title": "Titel bearbeiten",
"edit_user": "Nutzer bearbeiten",
"edit_workflow": "Workflow bearbeiten",
"editor": "Bearbeiten",
"editor": "Bearbeiter",
"editor_close_without_save_prompt": "Die Änderungen werden nicht gespeichert",
"editor_close_without_save_title": "Editor schließen?",
"editor_confirm_reset_all_changes": "Alle Änderungen zurücksetzen?",
"editor_flip_horizontal": "Horizontal spiegeln",
"editor_flip_vertical": "Vertikal spiegeln",
"editor_flip_horizontal": "horizontal spiegeln",
"editor_flip_vertical": "vertikal spiegeln",
"editor_orientation": "Ausrichtung",
"editor_reset_all_changes": "Änderungen zurücksetzen",
"editor_rotate_left": "Um 90° gegen den Uhrzeigersinn drehen",
@@ -1012,7 +1009,7 @@
"enabled": "Aktiviert",
"end_date": "Enddatum",
"enqueued": "Eingereiht",
"enter_wifi_name": "WiFi-Name eingeben",
"enter_wifi_name": "WLAN-Name eingeben",
"enter_your_pin_code": "PIN-Code eingeben",
"enter_your_pin_code_subtitle": "Gib deinen PIN-Code ein, um auf den gesperrten Ordner zuzugreifen",
"error": "Fehler",
@@ -1224,7 +1221,7 @@
"geolocation_instruction_location": "Klicke auf eine Datei mit GPS Koordinaten um diesen Standort zu verwenden oder wähle einen Standort direkt auf der Karte",
"get_help": "Hilfe erhalten",
"get_people_error": "Fehler beim Laden der Personen",
"get_wifiname_error": "WiFi-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WiFi-Netzwerk verbunden bist",
"get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist",
"getting_started": "Erste Schritte",
"go_back": "Zurück",
"go_to_folder": "Gehe zu Ordner",
@@ -1388,7 +1385,7 @@
"local_network_sheet_info": "Die App stellt über diese URL eine Verbindung zum Server her, wenn sie das angegebene WLAN-Netzwerk verwendet",
"location": "Standort",
"location_permission": "Standort Genehmigung",
"location_permission_content": "Um die automatische Umschaltfunktion nutzen zu können, benötigt Immich genaue Standortberechtigung, damit es den Namen des aktuellen WiFi-Netzwerks ermitteln kann",
"location_permission_content": "Um die automatische Umschaltfunktion nutzen zu können, benötigt Immich genaue Standortberechtigung, damit es den Namen des aktuellen WLAN-Netzwerks ermitteln kann",
"location_picker_choose_on_map": "Auf der Karte auswählen",
"location_picker_latitude_error": "Gültigen Breitengrad eingeben",
"location_picker_latitude_hint": "Breitengrad eingeben",
@@ -2203,7 +2200,7 @@
"theme_setting_asset_list_storage_indicator_title": "Fortschrittsbalken der Sicherung auf dem Vorschaubild",
"theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({count})",
"theme_setting_colorful_interface_subtitle": "Primärfarbe auf App-Hintergrund anwenden.",
"theme_setting_colorful_interface_title": "Farbige Benutzeroberfläche",
"theme_setting_colorful_interface_title": "Farbige UI-Oberfläche",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_primary_color_subtitle": "Farbauswahl für primäre Aktionen und Akzente.",
@@ -2298,7 +2295,6 @@
"upload_details": "Upload Details",
"upload_dialog_info": "Willst du die ausgewählten Elemente auf dem Server sichern?",
"upload_dialog_title": "Element hochladen",
"upload_error_with_count": "Uploadfehler für {count, plural, one {# asset} other {# assets}}",
"upload_errors": "Hochladen mit {count, plural, one {# Fehler} other {# Fehlern}} abgeschlossen, aktualisiere die Seite, um neu hochgeladene Dateien zu sehen.",
"upload_finished": "Upload fertig",
"upload_progress": "{remaining, number} verbleibend - {processed, number}/{total, number} verarbeitet",
@@ -2376,7 +2372,7 @@
"welcome": "Willkommen",
"welcome_to_immich": "Willkommen bei Immich",
"width": "Breite",
"wifi_name": "WiFi-Name",
"wifi_name": "WLAN-Name",
"workflow_delete_prompt": "Bist du sicher, dass du diesen Workflow löschen willst?",
"workflow_deleted": "Workflow gelöscht",
"workflow_description": "Workflow-Beschreibung",
@@ -2395,7 +2391,7 @@
"years_ago": "Vor {years, plural, one {einem Jahr} other {# Jahren}}",
"yes": "Ja",
"you_dont_have_any_shared_links": "Du hast keine geteilten Links",
"your_wifi_name": "Dein WiFi-Name",
"your_wifi_name": "Dein WLAN-Name",
"zero_to_clear_rating": "drücke 0 um die Dateibewertung zurückzusetzen",
"zoom_image": "Bild vergrößern",
"zoom_to_bounds": "Auf Grenzen zoomen"

View File

@@ -104,8 +104,6 @@
"image_preview_description": "Mez-granda bildo, sen metadatumoj, uzata por montri unuopan bildon, kaj por maŝin-lernado",
"image_preview_quality_description": "Kvalito de antaŭvido, inter 1 kaj 100. Pli alta numero indikas pli altan kvaliton, sed ankaŭ kreas pli grandajn dosierojn, kiuj povas malrapidigi uzadon de la apo. Tro malalta numero povas noci la maŝin-lernadon.",
"image_preview_title": "Agordoj pri antaŭvidoj",
"image_progressive": "Poiome",
"image_progressive_description": "Kodigi JPEG-bildojn por poioma vidigo dum ŝargado. Tio ŝanĝas nenion por WebP-bildoj.",
"image_quality": "Kvalito",
"image_resolution": "Distingivo",
"image_resolution_description": "Alta distingivo povas konservi pli da detaloj en bildoj sed postulas pli da tempo por trakti, donas pli grandajn dosierojn por stokie, kaj povas malrapidigi uzadon de la apo.",

View File

@@ -582,7 +582,7 @@
"asset_uploaded": "Subido",
"asset_uploading": "Subiendo…",
"asset_viewer_settings_subtitle": "Administra las configuraciones de tu visor de fotos",
"asset_viewer_settings_title": "Visor de archivos",
"asset_viewer_settings_title": "Visor de Archivos",
"assets": "elementos",
"assets_added_count": "{count, plural, one {# elemento añadido} other {# elementos añadidos}}",
"assets_added_to_album_count": "{count, plural, one {# elemento añadido} other {# elementos añadidos}} al álbum",
@@ -606,7 +606,7 @@
"assets_trashed_from_server": "{count} recurso(s) enviado(s) a la papelera desde el servidor de Immich",
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum",
"assets_were_part_of_albums_count": "{count, plural, one {El elemento ya es} other {Los elementos ya son}} parte de los álbumes",
"authorized_devices": "Dispositivos autorizados",
"authorized_devices": "Dispositivos Autorizados",
"automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares",
"automatic_endpoint_switching_title": "Cambio automático de URL",
"autoplay_slideshow": "Presentación con reproducción automática",
@@ -616,7 +616,7 @@
"background_location_permission": "Permiso de ubicación en segundo plano",
"background_location_permission_content": "Para poder cambiar de red mientras se ejecuta en segundo plano, Immich debe tener *siempre* acceso a la ubicación precisa para que la aplicación pueda leer el nombre de la red Wi-Fi",
"background_options": "Opciones de segundo plano",
"backup": "Copia de seguridad",
"backup": "Copia de Seguridad",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({count})",
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
"backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
@@ -1170,8 +1170,8 @@
"explorer": "Explorador",
"export": "Exportar",
"export_as_json": "Exportar a JSON",
"export_database": "Exportar base de datos",
"export_database_description": "Exportar la base de datos SQLite",
"export_database": "Exportar Base de Datos",
"export_database_description": "Exportar la Base de Datos SQLite",
"extension": "Extensión",
"external": "Externo",
"external_libraries": "Bibliotecas externas",
@@ -1236,7 +1236,7 @@
"group_places_by": "Agrupar lugares por...",
"group_year": "Agrupar por año",
"haptic_feedback_switch": "Activar respuesta háptica",
"haptic_feedback_title": "Respuesta háptica",
"haptic_feedback_title": "Respuesta Háptica",
"has_quota": "Cuota asignada",
"hash_asset": "Generar hash del archivo",
"hashed_assets": "Archivos con hash generado",
@@ -1864,7 +1864,7 @@
"reset_pin_code_description": "Si olvidaste tu código PIN, puedes comunicarte con el administrador del servidor para restablecerlo",
"reset_pin_code_success": "Código PIN restablecido correctamente",
"reset_pin_code_with_password": "Siempre puedes restablecer tu código PIN usando tu contraseña",
"reset_sqlite": "Restablecer la base de datos SQLite",
"reset_sqlite": "Restablecer la Base de Datos SQLite",
"reset_sqlite_confirmation": "¿Estás seguro que deseas restablecer la base de datos SQLite? Deberás cerrar sesión y volver a iniciarla para resincronizar los datos",
"reset_sqlite_success": "Restablecer exitosamente la base de datos SQLite",
"reset_to_default": "Restablecer los valores predeterminados",
@@ -2177,8 +2177,8 @@
"sync": "Sincronizar",
"sync_albums": "Sincronizar álbumes",
"sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los álbumes seleccionados a respaldar",
"sync_local": "Sincronización local",
"sync_remote": "Sincronización remota",
"sync_local": "Sincronización Local",
"sync_remote": "Sincronización Remota",
"sync_status": "Estado de la sincronización",
"sync_status_subtitle": "Ver y gestionar el estado de la sincronización",
"sync_upload_album_setting_subtitle": "Crea y sube tus fotos y videos a los álbumes seleccionados en Immich",

View File

@@ -572,7 +572,6 @@
"asset_list_layout_sub_title": "Asetus",
"asset_list_settings_subtitle": "Fotoruudustiku asetuse sätted",
"asset_list_settings_title": "Fotoruudustik",
"asset_not_found_on_device_android": "Üksust ei leitud seadmest",
"asset_offline": "Üksus pole kättesaadav",
"asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.",
"asset_restored_successfully": "Üksus edukalt taastatud",
@@ -1560,7 +1559,7 @@
"new_pin_code": "Uus PIN-kood",
"new_pin_code_subtitle": "See on sul esimene kord lukustatud kausta kasutada. Turvaliseks ligipääsuks loo PIN-kood",
"new_timeline": "Uus ajajoon",
"new_update": "Uus versioon",
"new_update": "Uus uuendus",
"new_user_created": "Uus kasutaja lisatud",
"new_version_available": "UUS VERSIOON SAADAVAL",
"newest_first": "Uuemad eespool",
@@ -2295,7 +2294,6 @@
"upload_details": "Üleslaadimise üksikasjad",
"upload_dialog_info": "Kas soovid valitud üksuse(d) serverisse varundada?",
"upload_dialog_title": "Üksuse üleslaadimine",
"upload_error_with_count": "Viga {count, plural, one {# üksuse} other {# üksuse}} üleslaadimisel",
"upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.",
"upload_finished": "Üleslaadimine lõpetatud",
"upload_progress": "Ootel {remaining, number} - Töödeldud {processed, number}/{total, number}",

View File

@@ -572,9 +572,6 @@
"asset_list_layout_sub_title": "Disposition",
"asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos",
"asset_list_settings_title": "Grille de photos",
"asset_not_found_on_device_android": "Média introuvable sur l'appareil",
"asset_not_found_on_device_ios": "Média introuvable sur l'appareil. Si vous utilisez iCloud, le média peut être inaccessible en raison d'un fichier corrompu stocké sur iCloud",
"asset_not_found_on_icloud": "Média introuvable sur iCloud. Le média est peut-être inaccessible en raison d'un fichier corrompu stocké sur iCloud",
"asset_offline": "Média hors ligne",
"asset_offline_description": "Ce média externe n'est plus accessible sur le disque. Veuillez contacter votre administrateur Immich pour obtenir de l'aide.",
"asset_restored_successfully": "Élément restauré avec succès",
@@ -2298,7 +2295,6 @@
"upload_details": "Détails des envois",
"upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur?",
"upload_dialog_title": "Envoyer le média",
"upload_error_with_count": "Erreur de chargement pour {count, plural, one {# média} other {# médias}}",
"upload_errors": "L'envoi s'est complété avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchissez la page pour voir les nouveaux médias envoyés.",
"upload_finished": "Envoi fini",
"upload_progress": "{remaining, number} restant(s) - {processed, number} traité(s)/{total, number}",

View File

@@ -104,8 +104,6 @@
"image_preview_description": "Immagine a media dimensione senza metadati, utilizzata durante la visualizzazione di una singola risorsa e per il machine learning",
"image_preview_quality_description": "Qualità dell'anteprima da 1 a 100. Più alto è meglio ma produce file più pesanti e può ridurre la reattività dell'app. Impostare un valore basso può influenzare negativamente la qualità del machine learning.",
"image_preview_title": "Impostazioni dell'anteprima",
"image_progressive": "Progressiva",
"image_progressive_description": "Codifica progressivamente le immagini JPEG per mostrarle con un caricamento graduale. Questo non ha effetto sulle immagini WebP.",
"image_quality": "Qualità",
"image_resolution": "Risoluzione",
"image_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedere più tempo per la codifica, avere dimensioni di file più grandi e ridurre la reattività dell'app.",
@@ -517,7 +515,6 @@
"all": "Tutti",
"all_albums": "Tutti gli album",
"all_people": "Tutte le persone",
"all_photos": "Tutte le foto",
"all_videos": "Tutti i video",
"allow_dark_mode": "Permetti Tema Scuro",
"allow_edits": "Permetti modifiche",
@@ -525,9 +522,6 @@
"allow_public_user_to_upload": "Permetti agli utenti pubblici di caricare",
"allowed": "Consentito",
"alt_text_qr_code": "Immagine QR",
"always_keep": "Mantieni sempre",
"always_keep_photos_hint": "Libera Spazio mantiene tutte le foto su questo dispositivo.",
"always_keep_videos_hint": "Libera Spazio mantiene tutti i video su questo dispositivo.",
"anti_clockwise": "Senso anti-orario",
"api_key": "Chiave API",
"api_key_description": "Questo valore verrà mostrato una sola volta. Assicurati di copiarlo prima di chiudere la finestra.",
@@ -572,9 +566,6 @@
"asset_list_layout_sub_title": "Layout",
"asset_list_settings_subtitle": "Impostazioni del layout della griglia delle foto",
"asset_list_settings_title": "Griglia foto",
"asset_not_found_on_device_android": "Risorsa non trovata sul dispositivo",
"asset_not_found_on_device_ios": "Risorsa non trovata sul dispositivo. Se stai usando iCloud, la risorsa potrebbe essere inaccessibile a causa di un file errato salvato su iCloud",
"asset_not_found_on_icloud": "Risorsa non trovata su iCloud. La risorsa potrebbe essere inaccessibile a causa di un file errato salvato su iCloud",
"asset_offline": "Elemento Offline",
"asset_offline_description": "Questa risorsa esterna non esiste più sul disco. Contatta il tuo amministratore di Immich per assistenza.",
"asset_restored_successfully": "Risorsa ripristinata con successo",
@@ -764,12 +755,11 @@
"cleanup_deleted_assets": "Spostate {count} risorse nel cestino",
"cleanup_deleting": "Spostamento nel cestino...",
"cleanup_found_assets": "Trovate {count} risorse già salvate",
"cleanup_found_assets_with_size": "Trovate {count} risorse salvate ({size})",
"cleanup_icloud_shared_albums_excluded": "Gli Album Condivisi di iCloud sono esclusi dalla ricerca",
"cleanup_no_assets_found": "Nessuna risorsa trovata con i criteri specificati. Libera Spazio può solo rimuovere le risorse che sono state salvate sul server",
"cleanup_no_assets_found": "Nessuna risorsa già salvata corrisponde ai criteri richiesti",
"cleanup_preview_title": "Risorse da rimuovere ({count})",
"cleanup_step3_description": "Ricerca risorse già salvate sul server corrispondenti alle opzioni di ricerca.",
"cleanup_step4_summary": "{count} risorse (create prima del {date}) da rimuovere sul tuo dispositivo. Rimarrano comunque accessibili dall'app Immich.",
"cleanup_step3_description": "Ricerca foto e video che sono stati già salvati sul server e che corrispondono alle opzioni di ricerca",
"cleanup_step4_summary": "{count} risorse create prima del {date} sono in coda per la rimozione dal dispositivo",
"cleanup_trash_hint": "Per recuperare completamente lo spazio devi aprire l'app della galleria e svuotarne il cestino",
"clear": "Pulisci",
"clear_all": "Pulisci tutto",
@@ -867,7 +857,7 @@
"custom_locale": "Localizzazione personalizzata",
"custom_locale_description": "Formatta data e numeri in base alla lingua e al paese",
"custom_url": "URL personalizzato",
"cutoff_date_description": "Mantieni le foto fino al…",
"cutoff_date_description": "Rimuovi foto e video più vecchi del",
"cutoff_day": "{count, plural, one {giorno} other {giorni}}",
"cutoff_year": "{count, plural, one {anno} other {anni}}",
"daily_title_text_date": "E, dd MMM",
@@ -1019,7 +1009,6 @@
"error_change_sort_album": "Errore nel cambiare l'ordine di degli album",
"error_delete_face": "Errore nella rimozione del volto dalla risorsa",
"error_getting_places": "Errore durante il recupero dei luoghi",
"error_loading_albums": "Errore nel caricamento degli album",
"error_loading_image": "Errore nel caricamento dell'immagine",
"error_loading_partners": "Errore durante il caricamento dei partner: {error}",
"error_retrieving_asset_information": "Errore nel recuperare informazioni sull'elemento",
@@ -1332,15 +1321,9 @@
"json_editor": "Modificatore JSON",
"json_error": "JSON errore",
"keep": "Mantieni",
"keep_albums": "Mantieni gli album",
"keep_albums_count": "{count} {count, plural, one {Album} other {Album}} mantenuti",
"keep_all": "Tieni tutto",
"keep_description": "Scegli cosa rimane sul tuo dispositivo quando liberi spazio.",
"keep_favorites": "Mantieni i favoriti",
"keep_on_device": "Mantieni sul dispositivo",
"keep_on_device_hint": "Seleziona le risorse da mantenere sul dispositivo",
"keep_this_delete_others": "Tieni questo, elimina gli altri",
"keeping": "Mantieni: {items}",
"kept_this_deleted_others": "Mantenuto questa risorsa ed {count, plural, one {eliminata # risorsa} other {eliminate # risorse}}",
"keyboard_shortcuts": "Scorciatoie da tastiera",
"language": "Lingua",
@@ -2295,7 +2278,6 @@
"upload_details": "Dettagli di caricamento",
"upload_dialog_info": "Vuoi fare il backup sul server delle risorse selezionate?",
"upload_dialog_title": "Carica Risorsa",
"upload_error_with_count": "Invio in errore per {count, plural, one {# risorsa} other {# risorse}}",
"upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere le risorse caricate.",
"upload_finished": "Upload terminato",
"upload_progress": "Rimanenti {remaining, number} - Processati {processed, number}/{total, number}",

View File

@@ -269,7 +269,7 @@
"oauth_auto_register": "자동 등록",
"oauth_auto_register_description": "OAuth 로그인 후 새 사용자를 자동으로 등록합니다.",
"oauth_button_text": "버튼 텍스트",
"oauth_client_secret_description": "비공개 클라이언트 또는 공개 클라이언트가 PKCE(Proof Key for Code Exchange, 코드 교환용 검증 키)를 지원하지 않는 경우 필요합니다.",
"oauth_client_secret_description": "OAuth 제공자가 PKCE(Proof Key for Code Exchange, 코드 교환용 검증 키)를 지원하지 않는 경우 필요합니다.",
"oauth_enable_description": "OAuth 로그인",
"oauth_mobile_redirect_uri": "모바일 리다이렉트 URI",
"oauth_mobile_redirect_uri_override": "모바일 리다이렉트 URI 오버라이드",
@@ -1180,7 +1180,7 @@
"folders_feature_description": "파일 시스템의 사진과 동영상을 폴더 보기로 탐색합니다.",
"forgot_pin_code_question": "PIN 번호를 잊어버렸나요?",
"forward": "앞으로",
"free_up_space_description": "백업된 사진과 동영상을 기기의 휴지통으로 이동하여 저장 공간을 확보하세요. 원본 파일은 서버에 안전하게 보관됩니다.",
"free_up_space_description": "백업된 사진과 동영상을 기기의 휴지통으로 이동하여 저장 공간을 확보하세요. 원본 파일은 서버에 안전하게 보관됩니다",
"full_path": "전체 경로: {path}",
"gcast_enabled": "구글 캐스트",
"gcast_enabled_description": "이 기능은 Google의 외부 리소스를 사용합니다.",

View File

@@ -1139,8 +1139,6 @@
"map_settings_only_show_favorites": "Rādīt tikai izlasi",
"map_settings_theme_settings": "Kartes Dizains",
"map_zoom_to_see_photos": "Attāliniet, lai redzētu fotoattēlus",
"mark_all_as_read": "Atzīmēt visus kā lasītus",
"marked_all_as_read": "Visi atzīmēti kā lasīti",
"matches": "Atbilstības",
"media_type": "Faila veids",
"memories": "Atmiņas",
@@ -1198,7 +1196,6 @@
"new_person": "Jauna persona",
"new_pin_code": "Jaunais PIN kods",
"new_timeline": "Jaunā laikjosla",
"new_update": "Pieejams atjauninājums",
"new_user_created": "Izveidots jauns lietotājs",
"new_version_available": "PIEEJAMA JAUNA VERSIJA",
"next": "Nākamais",
@@ -1490,7 +1487,7 @@
"select_album": "Izvēlies albumu",
"select_album_cover": "Izvēlieties albuma vāciņu",
"select_albums": "Izvēlies albumus",
"select_all_duplicates": "Atlasīt visus paturēšanai",
"select_all_duplicates": "Atlasīt visus dublikātus",
"select_avatar_color": "Izvēlies avatāra krāsu",
"select_face": "Izvēlies seju",
"select_from_computer": "Izvēlēties no datora",
@@ -1639,7 +1636,6 @@
"sort_title": "Nosaukums",
"source": "Pirmkods",
"stack": "Apvienot kaudzē",
"stack_duplicates": "Apvienot dublikātus kaudzē",
"start": "Sākt",
"start_date": "Sākuma datums",
"start_date_before_end_date": "Sākuma datumam jābūt pirms beigu datuma",
@@ -1722,7 +1718,6 @@
"unnamed_album": "Albums bez nosaukuma",
"unsaved_change": "Nesaglabāta izmaiņa",
"unselect_all": "Atcelt visu atlasi",
"unselect_all_duplicates": "Atlasīt visus dzēšanai",
"unstack": "At-Stekot",
"unsupported_field_type": "Nesatbalstīts lauka tips",
"untitled_workflow": "Nenosaukta darba plūsma",

View File

@@ -572,9 +572,6 @@
"asset_list_layout_sub_title": "Layout",
"asset_list_settings_subtitle": "Fotoraster layout instellingen",
"asset_list_settings_title": "Fotoraster",
"asset_not_found_on_device_android": "Item niet gevonden op apparaat",
"asset_not_found_on_device_ios": "Item niet gevonden op apparaat. Wanneer je iCloud gebruikt, kan het item niet toegankelijk zijn door een slecht bestand in iCloud",
"asset_not_found_on_icloud": "Item niet gevonden in iCloud. Het item kan ontoegankelijk zijn door een slecht bestand op iCloud",
"asset_offline": "Item offline",
"asset_offline_description": "Dit externe item is niet meer op de schijf te vinden. Neem contact op met de Immich beheerder voor hulp.",
"asset_restored_successfully": "Item succesvol hersteld",
@@ -869,7 +866,7 @@
"custom_url": "Aangepaste URL",
"cutoff_date_description": "Bewaar foto's van de laatste…",
"cutoff_day": "{count, plural, one {dag} other {dagen}}",
"cutoff_year": "{count, plural, one {jaar} other {jaar}}",
"cutoff_year": "{count, plural, one {jaar} other {jaren}}",
"daily_title_text_date": "E dd MMM",
"daily_title_text_date_year": "E dd MMM yyyy",
"dark": "Donker",
@@ -2298,7 +2295,6 @@
"upload_details": "Uploaddetails",
"upload_dialog_info": "Wil je een backup maken van de geselecteerde item(s) op de server?",
"upload_dialog_title": "Item uploaden",
"upload_error_with_count": "Upload fout voor {count, plural, one {# item} other {# items}}",
"upload_errors": "Upload voltooid met {count, plural, one {# fout} other {# fouten}}, vernieuw de pagina om de nieuwe items te zien.",
"upload_finished": "Uploaden is voltooid",
"upload_progress": "Resterend {remaining, number} - Verwerkt {processed, number}/{total, number}",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.5.2",
"version": "2.5.1",
"private": true,
"scripts": {
"format": "prettier --check .",

View File

@@ -272,7 +272,7 @@
"oauth_auto_register": "Automatyczna rejestracja",
"oauth_auto_register_description": "Automatycznie rejestruj nowych użytkowników po zalogowaniu się za pomocą protokołu OAuth",
"oauth_button_text": "Tekst na przycisku",
"oauth_client_secret_description": "Wymagane dla poufnego klienta lub jeśli PKCE (Proof Key for Code Exchange) nie jest obsługiwane dla klienta publicznego.",
"oauth_client_secret_description": "Wymagane jeżeli PKCE (Proof Key for Code Exchange) nie jest wspierane przez dostawcę OAuth",
"oauth_enable_description": "Loguj się za pomocą OAuth",
"oauth_mobile_redirect_uri": "Mobilny adres zwrotny",
"oauth_mobile_redirect_uri_override": "Zapasowy URI przekierowania mobilnego",
@@ -572,9 +572,6 @@
"asset_list_layout_sub_title": "Układ",
"asset_list_settings_subtitle": "Ustawienia układu siatki zdjęć",
"asset_list_settings_title": "Siatka Zdjęć",
"asset_not_found_on_device_android": "Nie znaleziono zasobu na urządzeniu",
"asset_not_found_on_device_ios": "Nie znaleziono zasobu na urządzeniu. Jeśli korzystasz z usługi iCloud, zasób może być niedostępny z powodu uszkodzonego pliku przechowywanego w usłudze iCloud",
"asset_not_found_on_icloud": "Nie znaleziono zasobu w usłudze iCloud. Zasób może być niedostępny z powodu uszkodzonego pliku przechowywanego w usłudze iCloud",
"asset_offline": "Zasób niedostępny",
"asset_offline_description": "Ten zewnętrzny zasób nie jest już dostępny na dysku. Aby uzyskać pomoc, skontaktuj się z administratorem Immich.",
"asset_restored_successfully": "Zasób został pomyślnie przywrócony",
@@ -1338,7 +1335,7 @@
"keep_description": "Wybierz, co zachować na Twoim urządzeniu przy zwalnianiu miejsca.",
"keep_favorites": "Zachowaj ulubione",
"keep_on_device": "Zachowaj na urządzeniu",
"keep_on_device_hint": "Wybierz elementy, które chcesz zachować na tym urządzeniu",
"keep_on_device_hint": "Wybierz , co zachować na tym urządzeniu",
"keep_this_delete_others": "Zachowaj to, usuń pozostałe",
"keeping": "Przechowano:{items}",
"kept_this_deleted_others": "Zachowano ten zasób i usunięto {count, plural, one {#zasób} other {#zasoby}}",
@@ -2298,7 +2295,6 @@
"upload_details": "Szczegóły przesyłania",
"upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobów na serwerze?",
"upload_dialog_title": "Prześlij Zasób",
"upload_error_with_count": "Błąd przesyłania dla {count, plural, one {# zasobu} other {# zasobów}}",
"upload_errors": "Przesyłanie zakończone z {count, plural, one {# błędem} other {# błędami}}. Odśwież stronę, aby zobaczyć nowo przesłane zasoby.",
"upload_finished": "Przesyłanie zakończone",
"upload_progress": "Pozostałe {remaining, number} - Przetworzone {processed, number}/{total, number}",

View File

@@ -104,8 +104,6 @@
"image_preview_description": "Imagem de tamanho médio sem metadados, utilizada ao visualizar um único ficheiro e pela aprendizagem de máquina",
"image_preview_quality_description": "Qualidade de pré-visualização de 1 a 100. Maior é melhor, mas produz ficheiros maiores e pode reduzir a capacidade de resposta da aplicação. Definir um valor demasiado baixo pode afetar a qualidade da aprendizagem de máquina.",
"image_preview_title": "Definições de Pré-visualização",
"image_progressive": "Progressivo",
"image_progressive_description": "Codificar imagens JPEG de forma progressiva para exibição com carregamento gradual. Não tem efeito em imagens WebP.",
"image_quality": "Qualidade",
"image_resolution": "Resolução",
"image_resolution_description": "Resoluções mais altas podem ajudar a preservar mais detalhes mas demoram mais a codificar, têm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.",
@@ -190,21 +188,10 @@
"machine_learning_smart_search_enabled": "Ativar a Pesquisa Inteligente",
"machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para Pesquisa Inteligente.",
"machine_learning_url_description": "A URL do servidor de aprendizagem de máquina. Se for fornecido mais do que um URL, cada servidor será testado, um a um, até um deles responder com sucesso, por ordem do primeiro ao último. Servidores que não responderem serão temporariamente ignorados até voltarem a estar online.",
"maintenance_delete_backup": "Eliminar Cópia de Segurança",
"maintenance_delete_backup_description": "Este ficheiro irá ser apagado para sempre.",
"maintenance_delete_error": "Ocorreu um erro ao eliminar a cópia de segurança.",
"maintenance_restore_backup": "Restaurar Cópia de Segurança",
"maintenance_restore_backup_description": "O Immich irá ser apagado e de seguida restaurado a partir da cópia de segurança selecionada. Irá ser criada uma cópia de segurança antes de continuar.",
"maintenance_restore_backup_different_version": "Esta cópia de segurança foi criada com uma versão diferente do Immich!",
"maintenance_restore_backup_unknown_version": "Não foi possível determinar a versão da cópia de segurança.",
"maintenance_restore_database_backup": "Restaurar cópia de seguraça da base de dados",
"maintenance_restore_database_backup_description": "Reverter para um estado anterior da base de dados utilizando um ficheiro de cópia de segurança",
"maintenance_settings": "Manutenção",
"maintenance_settings_description": "Colocar o Immich no modo de manutenção.",
"maintenance_start": "Aternar para o modo de manutenção",
"maintenance_start": "Iniciar modo de manutenção",
"maintenance_start_error": "Ocorreu um erro ao iniciar o modo de manutenção.",
"maintenance_upload_backup": "Carregar ficheiro de cópia de segurança da base de dados",
"maintenance_upload_backup_error": "Não foi possível carregar cópia de segurança. É um ficheiro .sql/.sql.gz?",
"manage_concurrency": "Gerir simultaneidade",
"manage_concurrency_description": "Navegar para a página das tarefas para gerir as tarefas em simultâneo",
"manage_log_settings": "Gerir definições de registo",
@@ -272,7 +259,7 @@
"oauth_auto_register": "Registo automático",
"oauth_auto_register_description": "Registar automaticamente novos utilizadores após iniciarem sessão com o OAuth",
"oauth_button_text": "Texto do botão",
"oauth_client_secret_description": "Obrigatório para o cliente confidencial, ou se a PKCE (Proof Key for Code Exchange) não for suportada para cliente público.",
"oauth_client_secret_description": "Obrigatório se PKCE (Proof Key for Code Exchange) não for suportado pelo provedor OAuth",
"oauth_enable_description": "Iniciar sessão com o OAuth",
"oauth_mobile_redirect_uri": "URI de redirecionamento móvel",
"oauth_mobile_redirect_uri_override": "Substituição de URI de redirecionamento móvel",
@@ -451,9 +438,6 @@
"admin_password": "Palavra-passe do administrador",
"administration": "Administração",
"advanced": "Avançado",
"advanced_settings_clear_image_cache": "Limpar a Cache de Imagens",
"advanced_settings_clear_image_cache_error": "Ocorreu um erro ao limpar a cache de imagens",
"advanced_settings_clear_image_cache_success": "Limpeza concluída com sucesso {size}",
"advanced_settings_enable_alternate_media_filter_subtitle": "Utilize esta definição para filtrar ficheiros durante a sincronização baseada em critérios alternativos. Utilize apenas se a aplicação estiver com problemas a detetar todos os álbuns.",
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar um filtro alternativo de sincronização de álbuns em dispositivos",
"advanced_settings_log_level_title": "Nível de registo: {level}",
@@ -517,7 +501,6 @@
"all": "Todos",
"all_albums": "Todos os álbuns",
"all_people": "Todas as pessoas",
"all_photos": "Todas as fotos",
"all_videos": "Todos os vídeos",
"allow_dark_mode": "Permitir modo escuro",
"allow_edits": "Permitir edições",
@@ -525,9 +508,6 @@
"allow_public_user_to_upload": "Permitir que utilizadores públicos façam carregamentos",
"allowed": "Permitido",
"alt_text_qr_code": "Imagem do código QR",
"always_keep": "Manter sempre",
"always_keep_photos_hint": "Libertar Espaço irá manter todas as fotos neste dispositivo.",
"always_keep_videos_hint": "Libertar Espaço irá manter todos os vídeos neste dispositivo.",
"anti_clockwise": "Sentido anti-horário",
"api_key": "Chave de API",
"api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.",
@@ -572,9 +552,6 @@
"asset_list_layout_sub_title": "Disposição",
"asset_list_settings_subtitle": "Configurações de disposição da grade de fotos",
"asset_list_settings_title": "Grade de fotos",
"asset_not_found_on_device_android": "Ficheiro não encontrado no dispositivo",
"asset_not_found_on_device_ios": "Ficheiro não encontrado no dispositivo. Se estiver a utilizar o iCloud, o ficheiro pode estar inacessível devido a um ficheiro corrompido armazenado no iCloud",
"asset_not_found_on_icloud": "Ficheiro não encontrado no iCloud. Este pode estar inacessível devido a um ficheiro corrompido armazenado no iCloud",
"asset_offline": "Ficheiro Indisponível",
"asset_offline_description": "Este ficheiro externo deixou de estar disponível no disco. Contacte o seu administrador do Immich para obter ajuda.",
"asset_restored_successfully": "FIcheiro restaurado com sucesso",
@@ -626,7 +603,7 @@
"backup_album_selection_page_select_albums": "Selecione Álbuns",
"backup_album_selection_page_selection_info": "Informações da Seleção",
"backup_album_selection_page_total_assets": "Total de ficheiros únicos",
"backup_albums_sync": "Cópia de Segurança de Sincronização de Álbuns",
"backup_albums_sync": "Cópia de segurança de sincronização de álbuns",
"backup_all": "Tudo",
"backup_background_service_backup_failed_message": "Ocorreu um erro ao efetuar cópia de segurança dos ficheiros. A tentar de novo…",
"backup_background_service_complete_notification": "Cópia de conteúdos concluída",
@@ -764,12 +741,11 @@
"cleanup_deleted_assets": "{count} ficheiro(s) foram movidos para a reciclagem do dispositivo",
"cleanup_deleting": "A mover para a reciclagem...",
"cleanup_found_assets": "Foram encontrados {count} ficheiro(s) com cópias de segurança",
"cleanup_found_assets_with_size": "Foram encontrados {count} ficheiros com cópia de segurança ({size})",
"cleanup_icloud_shared_albums_excluded": "Álbuns Partilhados do iCloud serão excluídos da pesquisa",
"cleanup_no_assets_found": "Nenhum ficheiro encontrado que siga os critérios acima. Libertar Espaço apenas pode remover ficheiros que tenham sido copiados para o servidor",
"cleanup_no_assets_found": "Nenhum ficheiro de cópia de segurança encontrado que siga os seus critérios",
"cleanup_preview_title": "Ficheiros a serem removidos ({count})",
"cleanup_step3_description": "Procurar por ficheiros no servidor que sigam os seus critérios de data e se serão mantidos.",
"cleanup_step4_summary": "{count} ficheiros (criados antes de {date}) para remover do seu dispositivo local. As fotos irão manter-se acessíveis através da aplicação do Immich.",
"cleanup_step3_description": "Procurar por fotos e vídeos que tenham sido copiados para o servidor com a data limite e as opções de filtro selecionadas",
"cleanup_step4_summary": "{count} ficheiros criados antes de {date} estão em espera para serem removidos do seu dispositivo",
"cleanup_trash_hint": "Para recuperar por completo o espaço de armazenamento, abra a aplicação da galeria do sistema e esvazie a reciclagem",
"clear": "Limpar",
"clear_all": "Limpar tudo",
@@ -867,7 +843,7 @@
"custom_locale": "Localização Personalizada",
"custom_locale_description": "Formatar datas e números baseados na língua e na região",
"custom_url": "URL personalizado",
"cutoff_date_description": "Manter fotos dos últimos…",
"cutoff_date_description": "Remover fotos e vídeos anteriores a",
"cutoff_day": "{count, plural, one {dia} other {dias}}",
"cutoff_year": "{count, plural, one {ano} other {anos}}",
"daily_title_text_date": "E, dd MMM",
@@ -1019,14 +995,11 @@
"error_change_sort_album": "Ocorreu um erro ao mudar a ordem de exibição",
"error_delete_face": "Falha ao remover rosto do ficheiro",
"error_getting_places": "Erro ao obter locais",
"error_loading_albums": "Ocorreu um erro ao carregar os álbuns",
"error_loading_image": "Erro ao carregar a imagem",
"error_loading_partners": "Erro ao carregar parceiros: {error}",
"error_retrieving_asset_information": "Ocorreu um erro ao carregar as informações do ficheiro",
"error_saving_image": "Erro: {error}",
"error_tag_face_bounding_box": "Erro ao marcar o rosto - não foi possível localizar o rosto",
"error_title": "Erro - Algo correu mal",
"error_while_navigating": "Ocorreu um erro ao navegar para o ficheiro",
"errors": {
"cannot_navigate_next_asset": "Não foi possível navegar para o próximo ficheiro",
"cannot_navigate_previous_asset": "Não foi possível navegar para o ficheiro anterior",
@@ -1215,7 +1188,7 @@
"forgot_pin_code_question": "Esqueceu-se do seu PIN?",
"forward": "Para a frente",
"free_up_space": "Libertar Espaço",
"free_up_space_description": "Mover fotos e vídeos que tenham sido copiados para o servidor para a reciclagem do seu dispositivo para libertar espaço. As cópias no servidor mantêm-se seguras.",
"free_up_space_description": "Mover fotos e vídeos que tenham sido copiados para o servidor para a reciclagem do seu dispositivo para libertar espaço. As cópias no servidor mantêm-se seguras",
"free_up_space_settings_subtitle": "Libertar espaço no dispositivo",
"full_path": "Caminho completo: {path}",
"gcast_enabled": "Google Cast",
@@ -1332,15 +1305,9 @@
"json_editor": "Editor JSON",
"json_error": "Erro JSON",
"keep": "Manter",
"keep_albums": "Manter álbuns",
"keep_albums_count": "A manter {count} {count, plural, one {álbum} other {álbuns}}",
"keep_all": "Manter Todos",
"keep_description": "Escolha o que fica no seu dispositivo quando liberta espaço.",
"keep_favorites": "Manter favoritos",
"keep_on_device": "Manter no dispositivo",
"keep_on_device_hint": "Selecionar itens para manter neste dispositivo",
"keep_this_delete_others": "Manter este ficheiro, eliminar os outros",
"keeping": "A manter: {items}",
"kept_this_deleted_others": "Foi mantido ficheiro e {count, plural, one {eliminado # outro} other {eliminados # outros}}",
"keyboard_shortcuts": "Atalhos do teclado",
"language": "Idioma",
@@ -1434,28 +1401,10 @@
"loop_videos_description": "Ativar para repetir os vídeos automaticamente durante a exibição.",
"main_branch_warning": "Está a usar uma versão de desenvolvimento; recomendamos vivamente que use uma versão de lançamento!",
"main_menu": "Menu Principal",
"maintenance_action_restore": "A Restaurar Base de Dados",
"maintenance_description": "O Immich foi colocado em <link>modo de manutenção</link>.",
"maintenance_end": "Desativar modo de manutenção",
"maintenance_end_error": "Ocorreu um erro ao desativar o modo de manutenção.",
"maintenance_logged_in_as": "Sessão iniciada como {user}",
"maintenance_restore_from_backup": "Restaurar a partir de uma cópia de segurança",
"maintenance_restore_library": "Restaurar a Sua Biblioteca",
"maintenance_restore_library_confirm": "Se isto parecer correto, continue para restaurar uma cópia de segurança!",
"maintenance_restore_library_description": "A Restaurar Base de Dados",
"maintenance_restore_library_folder_has_files": "{folder} tem {count} pasta(s)",
"maintenance_restore_library_folder_no_files": "{folder} tem ficheiros em falta!",
"maintenance_restore_library_folder_pass": "leitura e escrita possível",
"maintenance_restore_library_folder_read_fail": "leitura impossível",
"maintenance_restore_library_folder_write_fail": "escrita impossível",
"maintenance_restore_library_hint_missing_files": "Pode ter ficheiros importantes em falta",
"maintenance_restore_library_hint_regenerate_later": "Pode regenerá-las mais tarde nas definições",
"maintenance_restore_library_hint_storage_template_missing_files": "Está a utilizar um modelo de armazenamento? Pode ter ficheiros em falta",
"maintenance_restore_library_loading": "A carregar verificações de integradade e heurísticas…",
"maintenance_task_backup": "A criar uma cópia de segurança da base de dados existente…",
"maintenance_task_migrations": "A migrar base de dados…",
"maintenance_task_restore": "A restaurar a cópia de segurança selecionada…",
"maintenance_task_rollback": "Não foi possível restaurar, a reverter para o ponto de restauro…",
"maintenance_title": "Temporariamente Indisponível",
"make": "Marca",
"manage_geolocation": "Gerir localização",
@@ -1570,12 +1519,11 @@
"next_memory": "Próxima memória",
"no": "Não",
"no_actions_added": "Ainda não foram adicionadas ações",
"no_albums_found": "Nenhum álbum encontrado",
"no_albums_message": "Crie um álbum para organizar as suas fotos e vídeos",
"no_albums_with_name_yet": "Parece que ainda não tem nenhum álbum com este nome.",
"no_albums_yet": "Parece que ainda não tem nenhum álbum.",
"no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualização de fotos",
"no_assets_message": "Clique para carregar a sua primeira foto",
"no_assets_message": "FAÇA CLIQUE PARA CARREGAR A SUA PRIMEIRA FOTO",
"no_assets_to_show": "Não há ficheiros para exibir",
"no_cast_devices_found": "Nenhum dispositivo de transmissão encontrado",
"no_checksum_local": "Sem cálculo de verificação disponível - não pode capturar conteúdos locais",
@@ -1600,7 +1548,6 @@
"no_results_description": "Tente um sinónimo ou uma palavra-chave mais comum",
"no_shared_albums_message": "Crie um álbum para partilhar fotos e vídeos com pessoas na sua rede",
"no_uploads_in_progress": "Nenhum carregamento em curso",
"none": "Nenhum",
"not_allowed": "Não permitido",
"not_available": "N/A",
"not_in_any_album": "Não está em nenhum álbum",
@@ -1930,7 +1877,6 @@
"search_filter_media_type_title": "Selecione o tipo do ficheiro",
"search_filter_ocr": "Pesquisar por OCR",
"search_filter_people_title": "Selecionar pessoas",
"search_filter_star_rating": "Classificação",
"search_for": "Pesquisar por",
"search_for_existing_person": "Pesquisar por pessoas existentes",
"search_no_more_result": "Sem mais resultados",
@@ -2135,8 +2081,6 @@
"skip_to_folders": "Saltar para pastas",
"skip_to_tags": "Saltar para as etiquetas",
"slideshow": "Apresentação",
"slideshow_repeat": "Repetir apresentação de diapositivos",
"slideshow_repeat_description": "Repetir do inicio quando a apresentação acabar",
"slideshow_settings": "Definições de apresentação",
"sort_albums_by": "Ordenar álbuns por...",
"sort_created": "Data de criação",
@@ -2213,7 +2157,6 @@
"theme_setting_theme_subtitle": "Escolha a configuração do tema da aplicação",
"theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios pode aumentar o desempenho do carregamento, mas causa uma carga de rede significativamente maior",
"theme_setting_three_stage_loading_title": "Habilitar carregamento em três estágios",
"then": "Depois",
"they_will_be_merged_together": "Eles serão unidos",
"third_party_resources": "Recursos de terceiros",
"time": "Hora",
@@ -2269,7 +2212,6 @@
"unhide_person": "Exibir pessoa",
"unknown": "Desconhecido",
"unknown_country": "País desconhecido",
"unknown_date": "Data desconhecida",
"unknown_year": "Ano desconhecido",
"unlimited": "Ilimitado",
"unlink_motion_video": "Remover relação com video animado",
@@ -2298,7 +2240,6 @@
"upload_details": "Detalhes do Carregamento",
"upload_dialog_info": "Deseja realizar uma cópia de segurança dos ficheiros selecionados para o servidor?",
"upload_dialog_title": "Enviar ficheiro",
"upload_error_with_count": "Erro ao carregar {count, plural, one {# ficheiro} other {# ficheiros}}",
"upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos ficheiros enviados.",
"upload_finished": "Carregamento acabado",
"upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}",
@@ -2313,7 +2254,7 @@
"url": "URL",
"usage": "Utilização",
"use_biometric": "Utilizar dados biométricos",
"use_current_connection": "Utilizar a ligação atual",
"use_current_connection": "usar conexão atual",
"use_custom_date_range": "Utilizar um intervalo de datas personalizado",
"user": "Utilizador",
"user_has_been_deleted": "Este utilizador for eliminado.",

View File

@@ -572,9 +572,6 @@
"asset_list_layout_sub_title": "Rozvrhnutie",
"asset_list_settings_subtitle": "Nastavenia rozloženia mriežky fotografií",
"asset_list_settings_title": "Mriežka fotografií",
"asset_not_found_on_device_android": "Položka nebola nájdená v zariadení",
"asset_not_found_on_device_ios": "Položka nebola nájdená v zariadení. Ak používate iCloud, položka môže byť nedostupná kvôli poškodenému súboru uloženému v iCloude",
"asset_not_found_on_icloud": "Položka nebola nájdená v iCloude. Položka môže byť nedostupná kvôli poškodenému súboru uloženému v iCloude",
"asset_offline": "Médium je offline",
"asset_offline_description": "Tento externá položka sa už nenachádza na disku. Pre pomoc sa prosím obráťte na správcu systému Immich.",
"asset_restored_successfully": "Položky boli úspešne obnovené",
@@ -2298,7 +2295,6 @@
"upload_details": "Podrobnosti o nahrávaní",
"upload_dialog_info": "Chcete zálohovať zvolené médiá na server?",
"upload_dialog_title": "Nahrať médiá",
"upload_error_with_count": "Chyba pri nahrávaní {count, plural, one {# položky} few {# položiek} other {# položiek}}",
"upload_errors": "Nahrávanie ukončené s {count, plural, one {# chybou} other {# chybami}}, obnovte stránku, aby sa zobrazili nové položky.",
"upload_finished": "Nahrávanie dokončené",
"upload_progress": "Ostáva {remaining, number} - Spracovaných {processed, number}/{total, number}",

View File

@@ -572,9 +572,6 @@
"asset_list_layout_sub_title": "Postavitev",
"asset_list_settings_subtitle": "Nastavitve postavitve mreže fotografij",
"asset_list_settings_title": "Mreža fotografij",
"asset_not_found_on_device_android": "Sredstva ni bilo mogoče najti v napravi",
"asset_not_found_on_device_ios": "Sredstva ni bilo mogoče najti v napravi. Če uporabljate iCloud, sredstvo morda ni dostopno zaradi napačne datoteke, shranjene v iCloudu",
"asset_not_found_on_icloud": "Sredstva ni bilo mogoče najti v iCloudu. Sredstvo morda ni dostopno zaradi napačne datoteke, shranjene v iCloudu",
"asset_offline": "Sredstvo brez povezave",
"asset_offline_description": "Tega zunanjega sredstva ni več mogoče najti na disku. Za pomoč kontaktirajte Immich skrbnika.",
"asset_restored_successfully": "Sredstvo uspešno obnovljeno",
@@ -2298,7 +2295,6 @@
"upload_details": "Podrobnosti o nalaganju",
"upload_dialog_info": "Ali želite varnostno kopirati izbrana sredstva na strežnik?",
"upload_dialog_title": "Naloži sredstvo",
"upload_error_with_count": "Napaka pri prilaganju za {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}",
"upload_errors": "Nalaganje je končano s/z {count, plural, one {# napako} two {# napakama} other {# napakami}}, osvežite stran, da vidite nova sredstva za nalaganje.",
"upload_finished": "Nalaganje končano",
"upload_progress": "Preostalo {remaining, number} - Obdelano {processed, number}/{total, number}",

View File

@@ -540,42 +540,39 @@
"app_download_links": "APP下载链接",
"app_settings": "应用设置",
"app_stores": "应用商店",
"app_update_available": "应用更新已发布",
"appears_in": "收录于",
"apply_count": "应用 ({count, number})",
"app_update_available": "应用程序更新可用",
"appears_in": "所属相册",
"apply_count": "应用 ({count, number}个资产)",
"archive": "归档",
"archive_action_prompt": "已将 {count} 项添加到归档",
"archive_or_unarchive_photo": "归档或取消归档照片",
"archive_page_no_archived_assets": "未找到已归的资源",
"archive_page_no_archived_assets": "未找到归档资产",
"archive_page_title": "归档({count}",
"archive_size": "归档大小",
"archive_size_description": "配置下载归档大小GiB",
"archive_size_description": "配置下载归档大小GiB",
"archived": "已归档",
"archived_count": "{count, plural, other {已归档 # 项}}",
"are_these_the_same_person": "是同一个人吗?",
"are_you_sure_to_do_this": "确定执行此操作?",
"array_field_not_fully_supported": "数组字段需要手动进行 JSON 编辑",
"asset_action_delete_err_read_only": "无法删除只读资源,已跳过",
"asset_action_share_err_offline": "无法获取离线资源,已跳过",
"are_these_the_same_person": "他们是同一个人吗?",
"are_you_sure_to_do_this": "确定执行此操作?",
"array_field_not_fully_supported": "数组字段需要手动编辑 JSON",
"asset_action_delete_err_read_only": "无法删除只读资产,跳过",
"asset_action_share_err_offline": "无法获取离线资产,跳过",
"asset_added_to_album": "已添加至相册",
"asset_adding_to_album": "正在添加至相册…",
"asset_created": "资源已创建",
"asset_description_updated": "资描述已更新",
"asset_filename_is_offline": "资“{filename}”已离线",
"asset_has_unassigned_faces": "资源包含未分配的人脸",
"asset_hashing": "正在计算哈希值…",
"asset_list_group_by_sub_title": "分组依据",
"asset_created": "已创建资产",
"asset_description_updated": "资描述已更新",
"asset_filename_is_offline": "资“{filename}”已离线",
"asset_has_unassigned_faces": "资产中有未分配的人脸",
"asset_hashing": "哈希校验中…",
"asset_list_group_by_sub_title": "分组方式",
"asset_list_layout_settings_dynamic_layout_title": "动态布局",
"asset_list_layout_settings_group_automatically": "自动",
"asset_list_layout_settings_group_by": "资分组依据",
"asset_list_layout_settings_group_by_month_day": "月份 + 日期",
"asset_list_layout_settings_group_by": "资分组方式",
"asset_list_layout_settings_group_by_month_day": "月和日",
"asset_list_layout_sub_title": "布局",
"asset_list_settings_subtitle": "照片网格布局设置",
"asset_list_settings_title": "照片网格",
"asset_not_found_on_device_android": "设备上未找到该资源",
"asset_not_found_on_device_ios": "设备上未找到该资源。如果您使用了 iCloud可能是由于 iCloud 中存储了错误的文件导致资源无法访问",
"asset_not_found_on_icloud": "iCloud 中未找到该资源。可能是由于 iCloud 中存储了错误的文件导致资源无法访问",
"asset_offline": "资源离线",
"asset_offline": "资产脱机",
"asset_offline_description": "磁盘上已找不到该外部资产。请联系您的 Immich 管理员寻求帮助。",
"asset_restored_successfully": "已成功恢复所有资产",
"asset_skipped": "已跳过",
@@ -1520,7 +1517,7 @@
"mirror_horizontal": "水平",
"mirror_vertical": "垂直",
"missing": "缺失",
"mobile_app": "移动端APP",
"mobile_app": "手机APP",
"mobile_app_download_onboarding_note": "下载移动应用以访问这些选项",
"model": "型号",
"month": "月",
@@ -2298,7 +2295,6 @@
"upload_details": "上传详情",
"upload_dialog_info": "是否要将所选项目备份到服务器?",
"upload_dialog_title": "上传项目",
"upload_error_with_count": "{count, plural, one {# 个项目} other {# 个项目}}上传错误",
"upload_errors": "上传完成,出现{count, plural, one {#个错误} other {#个错误}},刷新页面以查看新上传的项目。",
"upload_finished": "上传完成",
"upload_progress": "剩余{remaining, number} - 已处理 {processed, number}/{total, number}",

View File

@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "2.5.2"
version = "2.5.1"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"

View File

@@ -919,7 +919,7 @@ wheels = [
[[package]]
name = "immich-ml"
version = "2.5.2"
version = "2.4.1"
source = { editable = "." }
dependencies = [
{ name = "aiocache" },

View File

@@ -75,7 +75,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
pnpm --prefix server run build
( cd ./open-api && bash ./bin/generate-open-api.sh )
uv version --directory machine-learning "$NEXT_SERVER"
uvx --from=toml-cli toml set --toml-path=machine-learning/pyproject.toml project.version "$NEXT_SERVER"
./misc/release/archive-version.js "$NEXT_SERVER"
fi

View File

@@ -1,18 +1,5 @@
experimental_monorepo_root = true
[monorepo]
config_roots = [
"plugins",
"server",
"cli",
"deployment",
"mobile",
"e2e",
"web",
"docs",
".github",
]
[tools]
node = "24.13.0"
flutter = "3.35.7"

View File

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

View File

@@ -741,7 +741,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -885,7 +885,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -915,7 +915,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -949,7 +949,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -992,7 +992,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1032,7 +1032,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1071,7 +1071,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1115,7 +1115,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1156,7 +1156,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CURRENT_PROJECT_VERSION = 233;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -79,7 +79,6 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
kCGImageSourceShouldCache: false,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceCreateThumbnailFromImageAlways: true
] as CFDictionary
func urlSession(

View File

@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.5.2</string>
<string>2.5.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -107,7 +107,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>240</string>
<string>233</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -39,14 +39,6 @@ iOS Release to TestFlight
iOS Manual Release
### ios gha_build_only
```sh
[bundle exec] fastlane ios gha_build_only
```
iOS Build Only (no TestFlight upload)
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.

View File

@@ -89,9 +89,7 @@ enum StoreKey<T> {
cleanupKeepMediaType<int>._(1009),
cleanupKeepAlbumIds<String>._(1010),
cleanupCutoffDaysAgo<int>._(1011),
cleanupDefaultsInitialized<bool>._(1012),
syncMigrationStatus<String>._(1013);
cleanupDefaultsInitialized<bool>._(1012);
const StoreKey._(this.id);
final int id;

View File

@@ -98,7 +98,12 @@ class AssetService {
height = fetched?.height?.toDouble();
}
return (width: width, height: height, isFlipped: false);
// Check exif for orientation to determine if dimensions should be flipped
// This is important for videos where raw file dimensions may not match display dimensions
final exif = await _remoteAssetRepository.getExif(asset.id);
final isFlipped = exif?.isFlipped ?? false;
return (width: width, height: height, isFlipped: isFlipped);
}
Future<List<(String, String)>> getPlaces(String userId) {

View File

@@ -1,7 +1,4 @@
// ignore_for_file: constant_identifier_names
import 'dart:async';
import 'dart:convert';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
@@ -10,21 +7,12 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
enum SyncMigrationTask {
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
}
class SyncStreamService {
final Logger _logger = Logger('SyncStreamService');
@@ -34,8 +22,6 @@ class SyncStreamService {
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager;
final StorageRepository _storageRepository;
final SyncMigrationRepository _syncMigrationRepository;
final ApiService _api;
final bool Function()? _cancelChecker;
SyncStreamService({
@@ -45,8 +31,6 @@ class SyncStreamService {
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
required SyncMigrationRepository syncMigrationRepository,
required ApiService api,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,
@@ -54,32 +38,12 @@ class SyncStreamService {
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository,
_syncMigrationRepository = syncMigrationRepository,
_api = api,
_cancelChecker = cancelChecker;
bool get isCancelled => _cancelChecker?.call() ?? false;
Future<bool> sync() async {
_logger.info("Remote sync request for user");
final serverVersion = await _api.serverInfoApi.getServerVersion();
if (serverVersion == null) {
_logger.severe("Cannot perform sync: unable to determine server version");
return false;
}
final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
final migrations = (jsonDecode(value) as List).cast<String>();
int previousLength = migrations.length;
await _runPreSyncTasks(migrations, semVer);
if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations");
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations));
}
// Start the sync stream and handle events
bool shouldReset = false;
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
@@ -87,56 +51,9 @@ class SyncStreamService {
_logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents);
}
previousLength = migrations.length;
await _runPostSyncTasks(migrations);
if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations");
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations));
}
return true;
}
Future<void> _runPreSyncTasks(List<String> migrations, SemVer semVer) async {
if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) {
_logger.info("Running pre-sync task: v20260128_ResetExifV1");
await _syncApiRepository.deleteSyncAck([
SyncEntityType.assetExifV1,
SyncEntityType.partnerAssetExifV1,
SyncEntityType.albumAssetExifCreateV1,
SyncEntityType.albumAssetExifUpdateV1,
]);
migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name);
}
if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) &&
semVer >= const SemVer(major: 2, minor: 5, patch: 0)) {
_logger.info("Running pre-sync task: v20260128_ResetAssetV1");
await _syncApiRepository.deleteSyncAck([
SyncEntityType.assetV1,
SyncEntityType.partnerAssetV1,
SyncEntityType.albumAssetCreateV1,
SyncEntityType.albumAssetUpdateV1,
]);
migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name);
if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) {
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}
}
Future<void> _runPostSyncTasks(List<String> migrations) async {
if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) {
_logger.info("Running post-sync task: v20260128_CopyExifWidthHeightToAsset");
await _syncMigrationRepository.v20260128CopyExifWidthHeightToAsset();
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
List<SyncEvent> items = [];
for (final event in events) {

View File

@@ -19,10 +19,6 @@ class SyncApiRepository {
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data));
}
Future<void> deleteSyncAck(List<SyncEntityType> types) {
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: types));
}
Future<void> streamChanges(
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
Function()? onReset,

View File

@@ -1,24 +0,0 @@
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SyncMigrationRepository extends DriftDatabaseRepository {
final Drift _db;
const SyncMigrationRepository(super.db) : _db = db;
Future<void> v20260128CopyExifWidthHeightToAsset() async {
await _db.customStatement('''
UPDATE remote_asset_entity
SET width = CASE
WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.height
ELSE exif.width
END,
height = CASE
WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.width
ELSE exif.height
END
FROM remote_exif_entity exif
WHERE exif.asset_id = remote_asset_entity.id
AND (exif.width IS NOT NULL OR exif.height IS NOT NULL);
''');
}
}

View File

@@ -92,9 +92,7 @@ class AssetViewer extends ConsumerStatefulWidget {
if (asset.isVideo || asset.isMotionPhoto) {
ref.read(videoPlaybackValueProvider.notifier).reset();
ref.read(videoPlayerControlsProvider.notifier).pause();
}
// Hide controls by default for videos
if (asset.isVideo) {
// Hide controls by default for videos and motion photos
ref.read(assetViewerProvider.notifier).setControls(false);
}
}
@@ -149,11 +147,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
if (asset != null) {
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
}
if (ref.read(assetViewerProvider).showingControls) {
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge));
} else {
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky));
}
}
@override

View File

@@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/services/hash.service.dart';
import 'package:immich_mobile/domain/services/local_sync.service.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
@@ -14,8 +13,6 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider)));
final syncStreamServiceProvider = Provider(
(ref) => SyncStreamService(
syncApiRepository: ref.watch(syncApiRepositoryProvider),
@@ -24,8 +21,6 @@ final syncStreamServiceProvider = Provider(
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider),
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
api: ref.watch(apiServiceProvider),
cancelChecker: ref.watch(cancellationProvider),
),
);

View File

@@ -28,7 +28,6 @@ import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
@@ -89,6 +88,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
if (version < 20 && Store.isBetaTimelineEnabled) {
await _syncLocalAlbumIsIosSharedAlbum(drift);
await _backfillAssetExifWidthHeight(drift);
}
if (targetVersion >= 12) {
@@ -282,6 +282,26 @@ Future<void> _syncLocalAlbumIsIosSharedAlbum(Drift db) async {
}
}
Future<void> _backfillAssetExifWidthHeight(Drift db) async {
try {
// Only backfill images (type = 1), not videos
// Videos have different dimension handling based on orientation/exif
await db.customStatement('''
UPDATE remote_exif_entity AS remote_exif
SET width = asset.width,
height = asset.height
FROM remote_asset_entity AS asset
WHERE remote_exif.asset_id = asset.id
AND asset.type = 1
AND (remote_exif.width IS NULL OR remote_exif.width = 0 OR remote_exif.height IS NULL OR remote_exif.height = 0);
''');
dPrint(() => "[MIGRATION] Successfully backfilled asset exif width and height");
} catch (error) {
dPrint(() => "[MIGRATION] Error while backfilling asset exif width and height: $error");
}
}
Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
try {
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();

View File

@@ -203,13 +203,9 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer {
void _decideIfWeAcceptEvent(PointerEvent event) {
final move = _initialFocalPoint! - _currentFocalPoint!;
// Accept gesture if movement is possible in the direction the user is swiping
final bool isHorizontalGesture = move.dx.abs() > move.dy.abs();
final bool shouldMove = isHorizontalGesture
? hitDetector!.shouldMove(move, Axis.horizontal)
: hitDetector!.shouldMove(move, Axis.vertical);
final bool shouldMove = validateAxis == Axis.vertical
? hitDetector!.shouldMove(move, Axis.vertical)
: hitDetector!.shouldMove(move, Axis.horizontal);
if (shouldMove || _pointerLocations.keys.length > 1) {
final double spanDelta = (_currentSpan! - _initialSpan!).abs();
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance;

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.5.2
- API version: 2.5.1
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 2.5.2+3033
version: 2.5.1+3032
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -4,5 +4,3 @@ import 'package:openapi/api.dart';
class MockAssetsApi extends Mock implements AssetsApi {}
class MockSyncApi extends Mock implements SyncApi {}
class MockServerApi extends Mock implements ServerApi {}

View File

@@ -19,15 +19,12 @@ import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
import '../../api.mocks.dart';
import '../../fixtures/asset.stub.dart';
import '../../fixtures/sync_stream.stub.dart';
import '../../infrastructure/repository.mock.dart';
import '../../mocks/asset_entity.mock.dart';
import '../../repository.mocks.dart';
import '../../service.mocks.dart';
class _AbortCallbackWrapper {
const _AbortCallbackWrapper();
@@ -53,9 +50,6 @@ void main() {
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo;
late LocalFilesManagerRepository mockLocalFilesManagerRepo;
late StorageRepository mockStorageRepo;
late MockApiService mockApi;
late MockServerApi mockServerApi;
late MockSyncMigrationRepository mockSyncMigrationRepo;
late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback;
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;
late _MockAbortCallbackWrapper mockResetCallbackWrapper;
@@ -88,9 +82,6 @@ void main() {
mockStorageRepo = MockStorageRepository();
mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
mockResetCallbackWrapper = _MockAbortCallbackWrapper();
mockApi = MockApiService();
mockServerApi = MockServerApi();
mockSyncMigrationRepo = MockSyncMigrationRepository();
when(() => mockAbortCallbackWrapper()).thenReturn(false);
@@ -103,12 +94,6 @@ void main() {
});
when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {});
when(() => mockSyncApiRepo.deleteSyncAck(any())).thenAnswer((_) async => {});
when(() => mockApi.serverInfoApi).thenReturn(mockServerApi);
when(() => mockServerApi.getServerVersion()).thenAnswer(
(_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0),
);
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
@@ -142,7 +127,6 @@ void main() {
when(() => mockSyncStreamRepo.deletePeopleV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler);
when(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset()).thenAnswer(successHandler);
sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo,
@@ -151,8 +135,6 @@ void main() {
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo,
api: mockApi,
syncMigrationRepository: mockSyncMigrationRepo,
);
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {});
@@ -234,8 +216,6 @@ void main() {
localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo,
cancelChecker: cancellationChecker.call,
api: mockApi,
syncMigrationRepository: mockSyncMigrationRepo,
);
await sut.sync();
@@ -275,8 +255,6 @@ void main() {
localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo,
cancelChecker: cancellationChecker.call,
api: mockApi,
syncMigrationRepository: mockSyncMigrationRepo,
);
await sut.sync();
@@ -496,7 +474,11 @@ void main() {
});
final events = [
SyncStreamStub.assetModified(id: 'remote-1', checksum: 'checksum-trash', ack: 'asset-remote-1-11'),
SyncStreamStub.assetModified(
id: 'remote-1',
checksum: 'checksum-trash',
ack: 'asset-remote-1-11',
),
];
await simulateEvents(events);
@@ -504,75 +486,4 @@ void main() {
verify(() => mockTrashedLocalAssetRepo.applyRestoredAssets(restoredIds)).called(1);
});
});
group('SyncStreamService - Sync Migration', () {
test('ensure that <2.5.0 migrations run', () async {
await Store.put(StoreKey.syncMigrationStatus, "[]");
when(
() => mockServerApi.getServerVersion(),
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1));
await sut.sync();
verifyInOrder([
() => mockSyncApiRepo.deleteSyncAck([
SyncEntityType.assetExifV1,
SyncEntityType.partnerAssetExifV1,
SyncEntityType.albumAssetExifCreateV1,
SyncEntityType.albumAssetExifUpdateV1,
]),
() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset(),
]);
// should only run on server >2.5.0
verifyNever(
() => mockSyncApiRepo.deleteSyncAck([
SyncEntityType.assetV1,
SyncEntityType.partnerAssetV1,
SyncEntityType.albumAssetCreateV1,
SyncEntityType.albumAssetUpdateV1,
]),
);
});
test('ensure that >=2.5.0 migrations run', () async {
await Store.put(StoreKey.syncMigrationStatus, "[]");
when(
() => mockServerApi.getServerVersion(),
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0));
await sut.sync();
verifyInOrder([
() => mockSyncApiRepo.deleteSyncAck([
SyncEntityType.assetExifV1,
SyncEntityType.partnerAssetExifV1,
SyncEntityType.albumAssetExifCreateV1,
SyncEntityType.albumAssetExifUpdateV1,
]),
() => mockSyncApiRepo.deleteSyncAck([
SyncEntityType.assetV1,
SyncEntityType.partnerAssetV1,
SyncEntityType.albumAssetCreateV1,
SyncEntityType.albumAssetUpdateV1,
]),
]);
// v20260128_ResetAssetV1 writes that v20260128_CopyExifWidthHeightToAsset has been completed
verifyNever(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset());
});
test('ensure that migrations do not re-run', () async {
await Store.put(
StoreKey.syncMigrationStatus,
'["${SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name}"]',
);
when(
() => mockServerApi.getServerVersion(),
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1));
await sut.sync();
verifyNever(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset());
});
});
}

View File

@@ -8,7 +8,6 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
@@ -47,8 +46,6 @@ class MockDriftBackupRepository extends Mock implements DriftBackupRepository {}
class MockUploadRepository extends Mock implements UploadRepository {}
class MockSyncMigrationRepository extends Mock implements SyncMigrationRepository {}
// API Repos
class MockUserApiRepository extends Mock implements UserApiRepository {}

View File

@@ -118,7 +118,13 @@ abstract final class TestUtils {
return result;
}
static domain.RemoteAsset createRemoteAsset({required String id, int? width, int? height, String? ownerId}) {
static domain.RemoteAsset createRemoteAsset({
required String id,
int? width,
int? height,
String? ownerId,
String? localId,
}) {
return domain.RemoteAsset(
id: id,
checksum: 'checksum1',
@@ -132,6 +138,7 @@ abstract final class TestUtils {
width: width,
height: height,
isEdited: false,
localId: localId,
);
}

View File

@@ -14951,7 +14951,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "2.5.2",
"version": "2.5.1",
"contact": {}
},
"tags": [

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "2.5.2",
"version": "2.5.1",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 2.5.2
* 2.5.1
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
"version": "2.5.2",
"version": "2.5.1",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "2.5.2",
"version": "2.5.1",
"description": "",
"author": "",
"private": true,

View File

@@ -79,7 +79,7 @@ export class MaintenanceWorkerService {
this.#secret = state.secret;
this.#status = {
active: true,
action: state.action?.action ?? MaintenanceAction.Start,
action: state.action.action,
};
StorageCore.setMediaLocation(this.detectMediaLocation());
@@ -88,10 +88,7 @@ export class MaintenanceWorkerService {
this.maintenanceWebsocketRepository.setStatusUpdateFn((status) => (this.#status = status));
await this.logSecret();
if (state.action) {
void this.runAction(state.action);
}
void this.runAction(state.action);
}
/**

View File

@@ -116,22 +116,8 @@ where
"asset"."deletedAt" is null
and "asset"."visibility" != $1
and (
not exists (
select
from
"asset_file"
where
"assetId" = "asset"."id"
and "asset_file"."type" = $2
)
or not exists (
select
from
"asset_file"
where
"assetId" = "asset"."id"
and "asset_file"."type" = $3
)
"asset_job_status"."previewAt" is null
or "asset_job_status"."thumbnailAt" is null
or "asset"."thumbhash" is null
)
@@ -306,14 +292,7 @@ from
where
"asset"."visibility" != $1
and "asset"."deletedAt" is null
and exists (
select
from
"asset_file"
where
"assetId" = "asset"."id"
and "asset_file"."type" = $2
)
and "job_status"."previewAt" is not null
and not exists (
select
from
@@ -644,14 +623,7 @@ from
where
"asset"."visibility" != $1
and "asset"."deletedAt" is null
and exists (
select
from
"asset_file"
where
"assetId" = "asset"."id"
and "asset_file"."type" = $2
)
and "job_status"."previewAt" is not null
order by
"asset"."fileCreatedAt" desc

View File

@@ -134,7 +134,8 @@ with
"asset"
inner join "asset_job_status" on "asset"."id" = "asset_job_status"."assetId"
where
(asset."localDateTime" at time zone 'UTC')::date = today.date
"asset_job_status"."previewAt" is not null
and (asset."localDateTime" at time zone 'UTC')::date = today.date
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."visibility" = $5
and exists (

View File

@@ -73,22 +73,8 @@ export class AssetJobRepository {
.innerJoin('asset_job_status', 'asset_job_status.assetId', 'asset.id')
.where((eb) =>
eb.or([
eb.not((eb) =>
eb.exists((qb) =>
qb
.selectFrom('asset_file')
.whereRef('assetId', '=', 'asset.id')
.where('asset_file.type', '=', AssetFileType.Preview),
),
),
eb.not((eb) =>
eb.exists((qb) =>
qb
.selectFrom('asset_file')
.whereRef('assetId', '=', 'asset.id')
.where('asset_file.type', '=', AssetFileType.Thumbnail),
),
),
eb('asset_job_status.previewAt', 'is', null),
eb('asset_job_status.thumbnailAt', 'is', null),
eb('asset.thumbhash', 'is', null),
]),
),
@@ -171,14 +157,7 @@ export class AssetJobRepository {
.where('asset.visibility', '!=', AssetVisibility.Hidden)
.where('asset.deletedAt', 'is', null)
.innerJoin('asset_job_status as job_status', 'assetId', 'asset.id')
.where((eb) =>
eb.exists((qb) =>
qb
.selectFrom('asset_file')
.whereRef('assetId', '=', 'asset.id')
.where('asset_file.type', '=', AssetFileType.Preview),
),
);
.where('job_status.previewAt', 'is not', null);
}
@GenerateSql({ params: [], stream: true })

View File

@@ -251,6 +251,8 @@ export class AssetRepository {
duplicatesDetectedAt: eb.ref('excluded.duplicatesDetectedAt'),
facesRecognizedAt: eb.ref('excluded.facesRecognizedAt'),
metadataExtractedAt: eb.ref('excluded.metadataExtractedAt'),
previewAt: eb.ref('excluded.previewAt'),
thumbnailAt: eb.ref('excluded.thumbnailAt'),
ocrAt: eb.ref('excluded.ocrAt'),
},
values[0],
@@ -359,6 +361,7 @@ export class AssetRepository {
.selectFrom('asset')
.selectAll('asset')
.innerJoin('asset_job_status', 'asset.id', 'asset_job_status.assetId')
.where('asset_job_status.previewAt', 'is not', null)
.where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
.where('asset.ownerId', '=', anyUuid(ownerIds))
.where('asset.visibility', '=', AssetVisibility.Timeline)

View File

@@ -1,11 +0,0 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "asset_job_status" DROP COLUMN "previewAt";`.execute(db);
await sql`ALTER TABLE "asset_job_status" DROP COLUMN "thumbnailAt";`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "asset_job_status" ADD "previewAt" timestamp with time zone;`.execute(db);
await sql`ALTER TABLE "asset_job_status" ADD "thumbnailAt" timestamp with time zone;`.execute(db);
}

View File

@@ -15,6 +15,12 @@ export class AssetJobStatusTable {
@Column({ type: 'timestamp with time zone', nullable: true })
duplicatesDetectedAt!: Timestamp | null;
@Column({ type: 'timestamp with time zone', nullable: true })
previewAt!: Timestamp | null;
@Column({ type: 'timestamp with time zone', nullable: true })
thumbnailAt!: Timestamp | null;
@Column({ type: 'timestamp with time zone', nullable: true })
ocrAt!: Timestamp | null;
}

View File

@@ -490,7 +490,7 @@ export interface MemoryData {
export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
export type SystemFlags = { mountChecks: Record<StorageFolder, boolean> };
export type MaintenanceModeState =
| { isMaintenanceMode: true; secret: string; action?: SetMaintenanceModeDto }
| { isMaintenanceMode: true; secret: string; action: SetMaintenanceModeDto }
| { isMaintenanceMode: false };
export type MemoriesState = {
/** memories have already been created through this date */

View File

@@ -601,6 +601,8 @@ const assetJobStatusInsert = (
duplicatesDetectedAt: date,
facesRecognizedAt: date,
metadataExtractedAt: date,
previewAt: date,
thumbnailAt: date,
};
return {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "2.5.2",
"version": "2.5.1",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -302,7 +302,6 @@
case AssetAction.ARCHIVE:
case AssetAction.DELETE:
case AssetAction.TRASH: {
const nextAsset = assetCursor.nextAsset ?? assetCursor.previousAsset;
assets.splice(
assets.findIndex((currentAsset) => currentAsset.id === action.asset.id),
1,
@@ -310,8 +309,10 @@
if (assets.length === 0) {
return await goto(Route.photos());
}
if (nextAsset) {
await navigateToAsset(nextAsset);
if (assetCursor.nextAsset) {
await navigateToAsset(assetCursor.nextAsset);
} else if (assetCursor.previousAsset) {
await navigateToAsset(assetCursor.previousAsset);
}
break;
}

View File

@@ -97,6 +97,7 @@
};
const handleClose = async (asset: { id: string }) => {
assetViewingStore.showAssetViewer(false);
invisible = true;
$gridScrollTarget = { at: asset.id };
await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget });

View File

@@ -141,41 +141,43 @@
}
};
const shortcutList = $derived.by(() => {
if (searchStore.isSearchEnabled || $showAssetViewer) {
return [];
}
let shortcutList = $derived(
(() => {
if (searchStore.isSearchEnabled || $showAssetViewer) {
return [];
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
{ shortcut: { key: '/' }, onShortcut: () => goto(Route.explore()) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
{ shortcut: { key: 'G' }, onShortcut: handleOpenDateModal },
];
if (onEscape) {
shortcuts.push({ shortcut: { key: 'Escape' }, onShortcut: onEscape });
}
const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
{ shortcut: { key: '/' }, onShortcut: () => goto(Route.explore()) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
{ shortcut: { key: 'G' }, onShortcut: handleOpenDateModal },
];
if (onEscape) {
shortcuts.push({ shortcut: { key: 'Escape' }, onShortcut: onEscape });
}
if (assetInteraction.selectionActive) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
);
}
if (assetInteraction.selectionActive) {
shortcuts.push(
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
);
}
return shortcuts;
});
return shortcuts;
})(),
);
</script>
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />

View File

@@ -3,13 +3,13 @@
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AlbumPageViewMode } from '$lib/constants';
import {
getAlbumActions,
handleRemoveUserFromAlbum,
handleUpdateAlbum,
handleUpdateUserAlbumRole,
} from '$lib/services/album.service';
import { user } from '$lib/stores/user.store';
import {
AlbumUserRole,
AssetOrder,
@@ -56,7 +56,7 @@
sharedLinks = sharedLinks.filter(({ id }) => sharedLink.id !== id);
};
const { AddUsers, CreateSharedLink } = $derived(getAlbumActions($t, album, AlbumPageViewMode.OPTIONS));
const { AddUsers, CreateSharedLink } = $derived(getAlbumActions($t, album));
let sharedLinks: SharedLinkResponseDto[] = $state([]);
@@ -108,9 +108,9 @@
<div class="ps-2">
<div class="flex items-center gap-2 mb-2">
<div>
<UserAvatar user={album.owner} size="md" />
<UserAvatar user={$user} size="md" />
</div>
<Text class="w-full" size="small">{album.owner.name}</Text>
<Text class="w-full" size="small">{$user.name}</Text>
<Field disabled class="w-32 shrink-0">
<Select options={[{ label: $t('owner'), value: 'owner' }]} value="owner" />
</Field>

View File

@@ -1,6 +1,5 @@
import { goto } from '$app/navigation';
import ToastAction from '$lib/components/ToastAction.svelte';
import { AlbumPageViewMode } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import AlbumAddUsersModal from '$lib/modals/AlbumAddUsersModal.svelte';
@@ -26,7 +25,7 @@ import {
type UserResponseDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiArrowLeft, mdiLink, mdiPlus, mdiPlusBoxOutline, mdiShareVariantOutline, mdiUpload } from '@mdi/js';
import { mdiLink, mdiPlus, mdiPlusBoxOutline, mdiShareVariantOutline, mdiUpload } from '@mdi/js';
import { type MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
@@ -40,7 +39,7 @@ export const getAlbumsActions = ($t: MessageFormatter) => {
return { Create };
};
export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto, viewMode: AlbumPageViewMode) => {
export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) => {
const isOwned = get(user).id === album.ownerId;
const Share: ActionItem = {
@@ -67,16 +66,7 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto, v
onAction: () => modalManager.show(SharedLinkCreateModal, { albumId: album.id }),
};
const Close: ActionItem = {
title: $t('go_back'),
type: $t('command'),
icon: mdiArrowLeft,
onAction: () => goto(Route.albums()),
$if: () => viewMode === AlbumPageViewMode.VIEW,
shortcuts: { key: 'Escape' },
};
return { Share, AddUsers, CreateSharedLink, Close };
return { Share, AddUsers, CreateSharedLink };
};
export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponseDto, assets: TimelineAsset[]) => {

View File

@@ -1,11 +1,9 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { user } from '$lib/stores/user.store';
import { asLocalTimeISO } from '$lib/utils/date-time';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
import { DateTime } from 'luxon';
import { get } from 'svelte/store';
type MemoryIndex = {
memoryIndex: number;
@@ -29,11 +27,6 @@ class MemoryStoreSvelte {
AuthLogout: () => this.clearCache(),
AuthUserLoaded: () => this.initialize(),
});
// loaded event might have already happened
if (get(user)) {
void this.initialize();
}
}
ready() {

View File

@@ -1,5 +1,5 @@
import { writable } from 'svelte/store';
import { getAlbumDateRange, getShortDateRange, timeToSeconds } from './date-time';
import { getAlbumDateRange, timeToSeconds } from './date-time';
describe('converting time to seconds', () => {
it('parses hh:mm:ss correctly', () => {
@@ -49,43 +49,6 @@ describe('converting time to seconds', () => {
});
});
describe('getShortDateRange', () => {
beforeEach(() => {
vi.stubEnv('TZ', 'UTC');
});
afterAll(() => {
vi.unstubAllEnvs();
});
it('should correctly return month if start and end date are within the same month', () => {
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('Jan 2022');
});
it('should correctly return month range if start and end date are in separate months within the same year', () => {
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan - Feb 2022');
});
it('should correctly return range if start and end date are in separate months and years', () => {
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 - Jan 2022');
});
it('should correctly return month if start and end date are within the same month, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC+6');
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('Jan 2022');
});
it('should correctly return month range if start and end date are in separate months within the same year, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC+6');
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan - Feb 2022');
});
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
vi.stubEnv('TZ', 'UTC+6');
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 - Jan 2022');
});
});
describe('getAlbumDate', () => {
beforeAll(() => {
process.env.TZ = 'UTC';

View File

@@ -19,30 +19,28 @@ export function parseUtcDate(date: string) {
return DateTime.fromISO(date, { zone: 'UTC' }).toUTC();
}
export const getShortDateRange = (startTimestamp: string, endTimestamp: string) => {
export const getShortDateRange = (startDate: string | Date, endDate: string | Date) => {
startDate = startDate instanceof Date ? startDate : new Date(startDate);
endDate = endDate instanceof Date ? endDate : new Date(endDate);
const userLocale = get(locale);
let startDate = DateTime.fromISO(startTimestamp).setZone('UTC');
let endDate = DateTime.fromISO(endTimestamp).setZone('UTC');
if (userLocale) {
startDate = startDate.setLocale(userLocale);
endDate = endDate.setLocale(userLocale);
}
const endDateLocalized = endDate.toLocaleString({
const endDateLocalized = endDate.toLocaleString(userLocale, {
month: 'short',
year: 'numeric',
// The API returns the date in UTC. If the earliest asset was taken on Jan 1st at 1am,
// we expect the album to start in January, even if the local timezone is UTC-5 for instance.
timeZone: 'UTC',
});
if (startDate.year === endDate.year) {
if (startDate.month === endDate.month) {
if (startDate.getFullYear() === endDate.getFullYear()) {
if (startDate.getMonth() === endDate.getMonth()) {
// Same year and month.
// e.g.: aug. 2024
return endDateLocalized;
} else {
// Same year but different month.
// e.g.: jul. - sept. 2024
const startMonthLocalized = startDate.toLocaleString({
const startMonthLocalized = startDate.toLocaleString(userLocale, {
month: 'short',
});
return `${startMonthLocalized} - ${endDateLocalized}`;
@@ -50,7 +48,7 @@ export const getShortDateRange = (startTimestamp: string, endTimestamp: string)
} else {
// Different year.
// e.g.: feb. 2021 - sept. 2024
const startDateLocalized = startDate.toLocaleString({
const startDateLocalized = startDate.toLocaleString(userLocale, {
month: 'short',
year: 'numeric',
});

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { goto, onNavigate } from '$app/navigation';
import { afterNavigate, goto, onNavigate } from '$app/navigation';
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
import ActionButton from '$lib/components/ActionButton.svelte';
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
@@ -52,7 +52,13 @@
import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { isAlbumsRoute, navigate, type AssetGridRouteSearchParams } from '$lib/utils/navigation';
import {
isAlbumsRoute,
isPeopleRoute,
isSearchRoute,
navigate,
type AssetGridRouteSearchParams,
} from '$lib/utils/navigation';
import { AlbumUserRole, AssetVisibility, getAlbumInfo, updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
import { CommandPaletteDefaultProvider, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
import {
@@ -85,6 +91,7 @@
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
let backUrl: string = $state(Route.albums());
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
let timelineManager = $state<TimelineManager>() as TimelineManager;
@@ -93,6 +100,25 @@
const assetInteraction = new AssetInteraction();
const timelineInteraction = new AssetInteraction();
afterNavigate(({ from }) => {
let url: string | undefined = from?.url?.pathname;
const route = from?.route?.id;
if (isSearchRoute(route)) {
url = from?.url.href;
}
if (isAlbumsRoute(route) || isPeopleRoute(route)) {
url = Route.albums();
}
backUrl = url || Route.albums();
if (backUrl === Route.sharedLinks()) {
backUrl = history.state?.backUrl || Route.albums();
}
});
const handleFavorite = async () => {
try {
await activityManager.toggleLike();
@@ -132,6 +158,7 @@
cancelMultiselect(assetInteraction);
return;
}
await goto(backUrl);
return;
};
@@ -278,7 +305,7 @@
const onAlbumDelete = async ({ id }: AlbumResponseDto) => {
if (id === album.id) {
await goto(Route.albums());
await goto(backUrl);
viewMode = AlbumPageViewMode.VIEW;
}
};
@@ -305,7 +332,7 @@
};
const { Cast } = $derived(getGlobalActions($t));
const { Share, Close } = $derived(getAlbumActions($t, album, viewMode));
const { Share } = $derived(getAlbumActions($t, album));
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineInteraction.selectedAssets));
</script>
@@ -319,7 +346,7 @@
onAlbumUserDelete={refreshAlbum}
onAlbumUpdate={(newAlbum) => (album = newAlbum)}
/>
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload, Close]} />
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload]} />
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: Route.albums() }}>
<div class="relative w-full shrink">
@@ -485,7 +512,7 @@
</AssetSelectControlBar>
{:else}
{#if viewMode === AlbumPageViewMode.VIEW}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(Route.albums())}>
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
{#snippet trailing()}
<ActionButton action={Cast} />

View File

@@ -33,6 +33,7 @@
import { Route } from '$lib/route';
import { getPersonActions } from '$lib/services/person.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { locale } from '$lib/stores/preferences.store';
import { preferences } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
@@ -60,6 +61,7 @@
let { data }: Props = $props();
let numberOfAssets = $derived(data.statistics.assets);
let { isViewing: showAssetViewer } = assetViewingStore;
let timelineManager = $state<TimelineManager>() as TimelineManager;
const options = $derived({ visibility: AssetVisibility.Timeline, personId: data.person.id });
@@ -104,13 +106,16 @@
});
const handleEscape = async () => {
if ($showAssetViewer) {
return;
}
if (assetInteraction.selectionActive) {
assetInteraction.clearMultiselect();
return;
} else {
await goto(previousRoute);
return;
}
await goto(previousRoute);
return;
};
const updateAssetCount = async () => {

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state';
import { shortcut } from '$lib/actions/shortcut';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
@@ -23,6 +24,7 @@
import type { Viewport } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { lang, locale } from '$lib/stores/preferences.store';
import { preferences, user } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils';
@@ -46,6 +48,7 @@
import { tick, untrack } from 'svelte';
import { t } from 'svelte-i18n';
let { isViewing: showAssetViewer } = assetViewingStore;
const viewport: Viewport = $state({ width: 0, height: 0 });
let searchResultsElement: HTMLElement | undefined = $state();
@@ -79,6 +82,18 @@
untrack(() => handlePromiseError(onSearchQueryUpdate()));
});
const onEscape = () => {
if ($showAssetViewer) {
return;
}
if (assetInteraction.selectionActive) {
assetInteraction.selectedAssets = [];
return;
}
handlePromiseError(goto(previousRoute));
};
$effect(() => {
if (scrollY) {
scrollYHistory = scrollY;
@@ -245,6 +260,7 @@
</script>
<svelte:window bind:scrollY />
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onEscape }} />
{#if terms}
<section