mirror of
https://github.com/immich-app/immich.git
synced 2026-01-28 15:55:03 -08:00
Compare commits
2 Commits
chore/tran
...
fix-migrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99a8740f1b | ||
|
|
a781c78caf |
@@ -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",
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
36
i18n/de.json
36
i18n/de.json
@@ -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"
|
||||
|
||||
@@ -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.",
|
||||
|
||||
18
i18n/es.json
18
i18n/es.json
@@ -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",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
26
i18n/it.json
26
i18n/it.json
@@ -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}",
|
||||
|
||||
@@ -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의 외부 리소스를 사용합니다.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --check .",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
79
i18n/pt.json
79
i18n/pt.json
@@ -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.",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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"
|
||||
|
||||
2
machine-learning/uv.lock
generated
2
machine-learning/uv.lock
generated
@@ -919,7 +919,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "2.5.2"
|
||||
version = "2.4.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -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
|
||||
|
||||
13
mise.toml
13
mise.toml
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -79,7 +79,6 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
||||
kCGImageSourceShouldCache: false,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true
|
||||
] as CFDictionary
|
||||
|
||||
func urlSession(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
''');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14951,7 +14951,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -601,6 +601,8 @@ const assetJobStatusInsert = (
|
||||
duplicatesDetectedAt: date,
|
||||
facesRecognizedAt: date,
|
||||
metadataExtractedAt: date,
|
||||
previewAt: date,
|
||||
thumbnailAt: date,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user