From b0af9be5139b885defb062d1458852e850702b8b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 5 Sep 2024 16:49:23 -0400 Subject: [PATCH 01/26] fix(web): person asset grid (#12370) --- .../[[assetId=id]]/+page.svelte | 84 +++++++++---------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 26e803deb6fd4..daa5821e8506b 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -76,13 +76,20 @@ isArchived: false, personId: data.person.id, }); + + $: person = data.person; + $: thumbnailData = getPeopleThumbnailUrl(person); + $: if (person) { + handlePromiseError(updateAssetCount()); + handlePromiseError(assetStore.updateOptions({ personId: person.id })); + } + const assetInteractionStore = createAssetInteractionStore(); const { selectedAssets, isMultiSelectState } = assetInteractionStore; let viewMode: ViewMode = ViewMode.VIEW_ASSETS; let isEditingName = false; let previousRoute: string = AppRoute.EXPLORE; - let previousPersonId: string = data.person.id; let people: PersonResponseDto[] = []; let personMerge1: PersonResponseDto; let personMerge2: PersonResponseDto; @@ -91,9 +98,6 @@ let refreshAssetGrid = false; let personName = ''; - $: thumbnailData = getPeopleThumbnailUrl(data.person); - - let name: string = data.person.name; let suggestedPeople: PersonResponseDto[] = []; /** @@ -120,8 +124,8 @@ } return websocketEvents.on('on_person_thumbnail', (personId: string) => { - if (data.person.id === personId) { - thumbnailData = getPeopleThumbnailUrl(data.person, Date.now().toString()); + if (person.id === personId) { + thumbnailData = getPeopleThumbnailUrl(person, Date.now().toString()); } }); }); @@ -141,7 +145,7 @@ const updateAssetCount = async () => { try { - const { assets } = await getPersonStatistics({ id: data.person.id }); + const { assets } = await getPersonStatistics({ id: person.id }); numberOfAssets = assets; } catch (error) { handleError(error, "Can't update the asset count"); @@ -150,20 +154,9 @@ afterNavigate(({ from }) => { // Prevent setting previousRoute to the current page. - if (from && from.route.id !== $page.route.id) { + if (from?.url && from.route.id !== $page.route.id) { previousRoute = from.url.href; } - if (previousPersonId !== data.person.id) { - handlePromiseError(updateAssetCount()); - assetStore.destroy(); - assetStore = new AssetStore({ - isArchived: false, - personId: data.person.id, - }); - previousPersonId = data.person.id; - name = data.person.name; - refreshAssetGrid = !refreshAssetGrid; - } }); const handleUnmerge = () => { @@ -179,8 +172,8 @@ const toggleHidePerson = async () => { try { await updatePerson({ - id: data.person.id, - personUpdateDto: { isHidden: !data.person.isHidden }, + id: person.id, + personUpdateDto: { isHidden: !person.isHidden }, }); notificationController.show({ @@ -208,7 +201,7 @@ return; } try { - await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); + await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info }); } catch (error) { handleError(error, $t('errors.unable_to_set_feature_photo')); @@ -233,7 +226,7 @@ type: NotificationType.Info, }); people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); - if (personToBeMergedIn.name != personName && data.person.id === personToBeMergedIn.id) { + if (personToBeMergedIn.name != personName && person.id === personToBeMergedIn.id) { await updateAssetCount(); refreshAssetGrid = !refreshAssetGrid; return; @@ -244,22 +237,22 @@ } }; - const handleSuggestPeople = (person: PersonResponseDto) => { + const handleSuggestPeople = (person2: PersonResponseDto) => { isEditingName = false; potentialMergePeople = []; personName = person.name; - personMerge1 = data.person; - personMerge2 = person; + personMerge1 = person; + personMerge2 = person2; viewMode = ViewMode.SUGGEST_MERGE; }; const changeName = async () => { viewMode = ViewMode.VIEW_ASSETS; - data.person.name = personName; + person.name = personName; try { isEditingName = false; - await updatePerson({ id: data.person.id, personUpdateDto: { name: personName } }); + await updatePerson({ id: person.id, personUpdateDto: { name: personName } }); notificationController.show({ message: $t('change_name_successfully'), @@ -283,7 +276,7 @@ potentialMergePeople = []; personName = name; - if (data.person.name === personName) { + if (person.name === personName) { return; } if (name === '') { @@ -294,12 +287,11 @@ const result = await searchPerson({ name: personName, withHidden: true }); const existingPerson = result.find( - (person: PersonResponseDto) => - person.name.toLowerCase() === personName.toLowerCase() && person.id !== data.person.id && person.name, + ({ name, id }: PersonResponseDto) => name.toLowerCase() === personName.toLowerCase() && id !== person.id && name, ); if (existingPerson) { personMerge2 = existingPerson; - personMerge1 = data.person; + personMerge1 = person; potentialMergePeople = result .filter( (person: PersonResponseDto) => @@ -318,10 +310,10 @@ const handleSetBirthDate = async (birthDate: string) => { try { viewMode = ViewMode.VIEW_ASSETS; - data.person.birthDate = birthDate; + person.birthDate = birthDate; const updatedPerson = await updatePerson({ - id: data.person.id, + id: person.id, personUpdateDto: { birthDate: birthDate.length > 0 ? birthDate : null }, }); @@ -354,7 +346,7 @@ {#if viewMode === ViewMode.UNASSIGN_ASSETS} a.id)} - personAssets={data.person} + personAssets={person} on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} on:confirm={handleUnmerge} /> @@ -373,14 +365,14 @@ {#if viewMode === ViewMode.BIRTH_DATE} (viewMode = ViewMode.VIEW_ASSETS)} on:updated={(event) => handleSetBirthDate(event.detail)} /> {/if} {#if viewMode === ViewMode.MERGE_PEOPLE} - handleMerge(detail)} /> + handleMerge(detail)} /> {/if}
@@ -394,7 +386,7 @@ assetStore.triggerUpdate()} /> - + (viewMode = ViewMode.SELECT_PERSON)} /> toggleHidePerson()} />
- {#key refreshAssetGrid} + {#key person.id} {#if isEditingName} handleNameChange(event.detail)} {thumbnailData} @@ -487,15 +479,15 @@ circle shadow url={thumbnailData} - altText={data.person.name} + altText={person.name} widthStyle="3.375rem" heightStyle="3.375rem" />
- {#if data.person.name} -

{data.person.name}

+ {#if person.name} +

{person.name}

{$t('assets_count', { values: { count: numberOfAssets } })}

From 649897f737a411b55a557b075c52ebb9a435d2ba Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 5 Sep 2024 23:57:12 +0200 Subject: [PATCH 02/26] docs: Add conditional album storage template information (#12218) --- docs/docs/partials/_storage-template.md | 6 ++++++ server/src/constants.ts | 1 + server/src/services/system-config.service.spec.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md index f48419c1ef7a6..b6dcd5ad7759c 100644 --- a/docs/docs/partials/_storage-template.md +++ b/docs/docs/partials/_storage-template.md @@ -27,3 +27,9 @@ If an asset is in multiple albums, `{{album}}` will be set to the name of the al ::: Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page. + +If you want to store assets in album folders, but you also have assets that do not belong to any album, you can use `{{#if album}}`, `{{else}}` and `{{/if}}` to create a conditional statement. For example, the following template will store assets in album folders if they belong to an album, and in a folder named "Other/Month" if they do not belong to an album: + +``` +{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}} +``` diff --git a/server/src/constants.ts b/server/src/constants.ts index 29ed5f6d37c85..6cfcc41d89ba6 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -75,6 +75,7 @@ export const supportedPresetTokens = [ '{{y}}/{{MM}}-{{dd}}/{{filename}}', '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}/{{filename}}', + '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}', '{{y}}/{{MMM}}/{{filename}}', '{{y}}/{{MMMM}}/{{filename}}', '{{y}}/{{MM}}/{{dd}}/{{filename}}', diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index af2b564ab2fe8..409cd6a52f360 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -336,6 +336,7 @@ describe(SystemConfigService.name, () => { '{{y}}/{{MM}}-{{dd}}/{{filename}}', '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}/{{filename}}', + '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}', '{{y}}/{{MMM}}/{{filename}}', '{{y}}/{{MMMM}}/{{filename}}', '{{y}}/{{MM}}/{{dd}}/{{filename}}', From eb7777639d70a584702b679888411fab07c36678 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:09:19 -0400 Subject: [PATCH 03/26] fix(server): clean face tables after delete (#12375) clean face tables after delete --- server/src/repositories/person.repository.ts | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 7459ca318348e..1290df740e62d 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -6,6 +6,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SourceType } from 'src/enum'; import { AssetFaceId, DeleteAllFacesOptions, @@ -53,16 +54,20 @@ export class PersonRepository implements IPersonRepository { } async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise { - if (sourceType) { - await this.assetFaceRepository - .createQueryBuilder('asset_faces') - .delete() - .andWhere('sourceType = :sourceType', { sourceType }) - .execute(); - return; + if (!sourceType) { + return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); } - await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); + await this.assetFaceRepository + .createQueryBuilder('asset_faces') + .delete() + .andWhere('sourceType = :sourceType', { sourceType }) + .execute(); + + await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search'); + if (sourceType === SourceType.MACHINE_LEARNING) { + await this.assetFaceRepository.query('REINDEX INDEX face_index'); + } } getAllFaces( From 02803816f479cb6983bbf1de7204a8f0e8f2be50 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 6 Sep 2024 01:57:27 +0200 Subject: [PATCH 04/26] chore(web): update translations (#12265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: Adrian M Co-authored-by: Anthony MARGERAND Co-authored-by: Bezruchenko Simon Co-authored-by: Denis Pacquier Co-authored-by: Florian Ostertag Co-authored-by: Javier Montón Co-authored-by: Jonathan Jogenfors Co-authored-by: Mathias Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Miki Mrvos Co-authored-by: Nicolai Bonde Co-authored-by: S Kutu Co-authored-by: Shawn Co-authored-by: Xo Co-authored-by: chapvic Co-authored-by: dvbthien Co-authored-by: gallegonovato Co-authored-by: pyccl Co-authored-by: waclaw66 --- web/src/lib/i18n/cs.json | 7 ++- web/src/lib/i18n/da.json | 53 +++++++++++++++------ web/src/lib/i18n/de.json | 7 ++- web/src/lib/i18n/es.json | 9 +++- web/src/lib/i18n/fr.json | 7 ++- web/src/lib/i18n/he.json | 1 + web/src/lib/i18n/nl.json | 9 +++- web/src/lib/i18n/ro.json | 14 ++++-- web/src/lib/i18n/ru.json | 9 +++- web/src/lib/i18n/sr_Cyrl.json | 1 + web/src/lib/i18n/sr_Latn.json | 1 + web/src/lib/i18n/sv.json | 32 +++++++------ web/src/lib/i18n/tr.json | 71 ++++++++++++++++++----------- web/src/lib/i18n/uk.json | 1 + web/src/lib/i18n/vi.json | 23 ++++++---- web/src/lib/i18n/zh_SIMPLIFIED.json | 11 ++++- 16 files changed, 182 insertions(+), 74 deletions(-) diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index 1a906e5134081..ceb692a736685 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -139,7 +139,11 @@ "map_settings_description": "Správa nastavení mapy", "map_style_description": "URL na style.json motivu", "metadata_extraction_job": "Extrakce metadat", - "metadata_extraction_job_description": "Získání informací o metadatech z každého snímku, jako je GPS a rozlišení", + "metadata_extraction_job_description": "Získání informací o metadatech z každého snímku, jako je GPS, obličeje a rozlišení", + "metadata_faces_import_setting": "Povolit import obličeje", + "metadata_faces_import_setting_description": "Import obličejů z EXIF dat obrázků a sidecar souborů", + "metadata_settings": "Metadata", + "metadata_settings_description": "Správa nastavení metadat", "migration_job": "Migrace", "migration_job_description": "Migrace miniatur snímků a obličejů do nejnovější struktury složek", "no_paths_added": "Nebyly přidány žádné cesty", @@ -1260,6 +1264,7 @@ "to_change_password": "Změnit heslo", "to_favorite": "Oblíbit", "to_login": "Přihlásit", + "to_parent": "Přejít k rodiči", "to_root": "Přejít ke kořenu", "to_trash": "Vyhodit", "toggle_settings": "Přepnout nastavení", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index d07549b463028..eb9d99d074c10 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -5,7 +5,7 @@ "acknowledge": "Anerkend", "action": "Handling", "actions": "Handlinger", - "active": "Aktiv", + "active": "Aktive", "activity": "Aktivitet", "activity_changed": "Aktivitet er {enabled, select, true {aktiveret} other {deaktiveret}}", "add": "Tilføj", @@ -129,7 +129,7 @@ "map_enable_description": "Aktivér kortfunktioner", "map_gps_settings": "Kort- og GPS-indstillinger", "map_gps_settings_description": "Håndter indstillinger for Kort og GPS (Omvendt Geokodning)", - "map_implications": "Kortfunktionen afhænger af en ekstern felt-service (tiles.immich.cloud)", + "map_implications": "Kortfunktionen afhænger af en ekstern tile-service (tiles.immich.cloud)", "map_light_style": "Lyst tema", "map_manage_reverse_geocoding_settings": "Håndtér indstillinger for Omvendt Geokoding", "map_reverse_geocoding": "Omvendt geokodning", @@ -174,7 +174,7 @@ "oauth_issuer_url": "Udsteder-URL", "oauth_mobile_redirect_uri": "Mobilomdiregerings-URL", "oauth_mobile_redirect_uri_override": "Tilsidesættelse af mobil omdiregerings-URL", - "oauth_mobile_redirect_uri_override_description": "Slå til når \"app.immich:/\" er en ugyldig omdiregerings-URL.", + "oauth_mobile_redirect_uri_override_description": "Aktiver, når OAuth-udbyderen ikke tillader en mobil URI, som '{callback}'", "oauth_profile_signing_algorithm": "Log-ind-algoritme", "oauth_profile_signing_algorithm_description": "Algoritme til signering af brugerprofilen.", "oauth_scope": "Omfang", @@ -225,9 +225,14 @@ "storage_template_hash_verification_enabled_description": "Slår hash-verifikation til, slå ikke dette fra med mindre du er sikker på dets konsekvenser", "storage_template_migration": "Lagringsskabelonmigration", "storage_template_migration_description": "Anvend den nuværende {template} på tidligere uploadede mediefiler", - "storage_template_migration_job": "Lagringsmigrationsopgave", + "storage_template_migration_info": "Skabelonændringer vil kun gælde for nye mediefiler. For at anvende skabelonen retroaktivt på tidligere uploadede mediefiler skal du køre {job}.", + "storage_template_migration_job": "Lager Skabelon Migrationsjob", + "storage_template_more_details": "For flere detaljer om denne funktion, referer til Lager Skabelonen og dens implikationer", + "storage_template_onboarding_description": "Når denne funktion er aktiveret, vil den automatisk organisere filer baseret på en brugerdefineret skabelon. På grund af stabilitetsproblemer er funktionen som standard slået fra. For mere information, se dokumentation.", + "storage_template_path_length": "Anslået sti-længde begrænsning {length, number}/{limit, number}", "storage_template_settings": "Lagringsskabelon", "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil", + "storage_template_user_label": "{label} er brugerens Lagringsmærkat", "system_settings": "Systemindstillinger", "theme_custom_css_settings": "Brugerdefineret CSS", "theme_custom_css_settings_description": "Cascading Style Sheets tillader at give Immich et brugerdefineret look.", @@ -245,12 +250,15 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Accepterede lyd-codecs", "transcoding_accepted_audio_codecs_description": "Vælg hvilke lyd-codecs der ikke behøver at blive transkodet. Bruges kun ved bestemte transkodningspolitikker.", + "transcoding_accepted_containers": "Accepterede containere", + "transcoding_accepted_containers_description": "Vælg hvilke containerformater der ikke skal remuxes til MP4. Bruges kun for visse transkodningspolitiker.", "transcoding_accepted_video_codecs": "Accepterede video-codecs", "transcoding_accepted_video_codecs_description": "Vælg hvilke video-codec der ikke behøver at bliver transkodet. Bruges kun ved bestemte transkodningspolitikker.", "transcoding_advanced_options_description": "Indstillinger de fleste brugere ikke behøver at ændre på", "transcoding_audio_codec": "Lyd-codec", "transcoding_audio_codec_description": "Opus er den indstillingen med højest kvalitet, men har mindre kompatibilitet med gamle enheder og software.", "transcoding_bitrate_description": "Videoer med højere end maksimumbitrate eller ikke i et godkendt format", + "transcoding_codecs_learn_more": "For at lære mere om den terminologi der bruges her, henvises til FFmpeg dokumentationen for H.264 codec, HEVC codec og VP9 codec.", "transcoding_constant_quality_mode": "Konstant kvalitetstilstand", "transcoding_constant_quality_mode_description": "ICQ er bedre end CQP, men nogle hardwareaccelerationsenheder understøtter ikke denne tilstand. At slå denne indstilling til vil foretrække den specificerede tilstand når kvalitetsbaseret indkodning bruges. Ignoreret at NVENC, da det ikke understøtter ICQ.", "transcoding_constant_rate_factor": "Konstant ratefaktor (-crf)", @@ -259,7 +267,7 @@ "transcoding_hardware_acceleration": "Hardwareacceleration", "transcoding_hardware_acceleration_description": "Eksperimentel; meget hurtigere, men vil have lavere kvalitet ved samme bitrate", "transcoding_hardware_decoding": "Hardware-afkodning", - "transcoding_hardware_decoding_setting_description": "Gælder kun NVENC og RKMPP. Slår ende-til-ende acceleration til i stedet for kun at accelerere indkodning. Virker måske ikke på alle videoer.", + "transcoding_hardware_decoding_setting_description": "Gælder kun NVENC, QSV og RKMPP. Slår ende-til-ende acceleration til i stedet for kun at accelerere indkodning. Virker måske ikke på alle videoer.", "transcoding_hevc_codec": "HEVC-codec", "transcoding_max_b_frames": "Maksimum B-frames", "transcoding_max_b_frames_description": "Højere værdier forbedrer kompressionseffektivitet, men kan gøre indkodning langsommere. Er måske ikke kompatibelt med hardware-acceleration på ældre enheder. 0 slår B-frames fra, mens -1 sætter denne værdi automatisk.", @@ -300,15 +308,21 @@ "trash_settings_description": "Administrér skraldeindstillinger", "untracked_files": "Utrackede filer", "untracked_files_description": "Applikationen holder ikke styr på disse filer. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller være efterladt på grund af en fejl", + "user_delete_delay": "{user}'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.", "user_delete_delay_settings": "Slet forsinkelse", "user_delete_delay_settings_description": "Antal dage efter fjernelse for permanent at slette en brugers konto og mediefiler. Opgaven for sletning af brugere kører ved midnat for at tjekke efter brugere, der er klar til sletning. Ændringer i denne indstilling vil blive evalueret ved næste udførelse.", + "user_delete_immediately": "{user}'s konto og aktiver vil blive sat i kø til permanent sletning med det samme.", + "user_delete_immediately_checkbox": "Sæt bruger og aktiver i kø til øjeblikkelig sletning", "user_management": "Brugeradministration", "user_password_has_been_reset": "Brugerens adgangskode er blevet nulstillet:", "user_password_reset_description": "Venligst oplys brugeren om den midlertidige adgangskode og informér dem, at de vil være nødt til at ændre adgangskoden ved næste login.", + "user_restore_description": "{user}'s konto vil blive gendannet.", + "user_restore_scheduled_removal": "Gendan bruger - planlagt fjernelse den {date, date, long}", "user_settings": "Brugerindstillinger", "user_settings_description": "Administrér brugerindstillinger", "user_successfully_removed": "Bruger {email} er blevet fjernet med succes.", - "version_check_enabled_description": "Aktiver periodiske forespørgsler til GitHub for at tjekke efter nye versioner", + "version_check_enabled_description": "Aktivér versionstjek", + "version_check_implications": "Funktionen til versionstjek er afhængig af periodisk kommunikation med github.com", "version_check_settings": "Versiontjek", "version_check_settings_description": "Aktiver/deaktier notifikation for den nye version", "video_conversion_job": "Transkod videoer", @@ -318,12 +332,21 @@ "admin_password": "Administratoradgangskode", "administration": "Administration", "advanced": "Avanceret", + "age_months": "Alder {months, plural, one {# month} other {# months}}", + "age_year_months": "Alder 1 år, {months, plural, one {# måned} other {# måneder}}", + "age_years": "{years, plural, other {Alder #}}", "album_added": "Album tilføjet", "album_added_notification_setting_description": "Modtag en emailnotifikation når du bliver tilføjet til en delt album", "album_cover_updated": "Albumcover opdateret", + "album_delete_confirmation": "Er du sikker på at du vil slette albummet {album}?", + "album_delete_confirmation_description": "Hvis dette album er delt, vil andre brugere ikke længere kunne få adgang til det.", "album_info_updated": "Albuminfo opdateret", + "album_leave": "Forlad albummet?", + "album_leave_confirmation": "Er du sikker på at du vil forlade {album}?", "album_name": "Albumnavn", "album_options": "Albumindstillinger", + "album_remove_user": "Fjern bruger?", + "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?", "album_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", "albums": "Albummer", @@ -338,6 +361,8 @@ "appears_in": "Optræder i", "archive": "Arkiv", "archive_or_unarchive_photo": "Arkivér eller dearkivér billede", + "archive_size": "Arkiv størelse", + "archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)", "archived": "Arkiveret", "asset_offline": "Mediefil offline", "assets": "elementer", @@ -361,7 +386,7 @@ "change_location": "Ændr sted", "change_name": "Ændr navn", "change_name_successfully": "Navn ændret med succes", - "change_password": "Skift kodeord", + "change_password": "Skift Kodeord", "change_your_password": "Skift din adgangskode", "changed_visibility_successfully": "Ændrede synlighed med succes", "check_all": "Tjek alle", @@ -440,6 +465,8 @@ "display_original_photos_setting_description": "Foretræk at vise det originale billede frem for miniaturebilleder når den originale fil er web-kompatibelt. Dette kan gøre billedvisning langsommere.", "done": "Færdig", "download": "Hent", + "download_settings": "Download", + "download_settings_description": "Administrer indstillinger relateret til mediefil-downloads", "downloading": "Downloader", "duplicates": "Duplikater", "duration": "Varighed", @@ -723,9 +750,9 @@ "password_required": "Adgangskode påkrævet", "password_reset_success": "Nulstilling af adgangskode succes", "past_durations": { - "days": "Sidste {days, plural, one {dag} other {{days, number} dage}}", - "hours": "Sidste {hours, plural, one {time} other {{hours, number} timer}}", - "years": "Sidste {years, plural, one {år} other {{years, number} år}}" + "days": "Sidste {days, plural, one {dag} other {# dage}}", + "hours": "Sidste {hours, plural, one {time} other {# timer}}", + "years": "Sidste {years, plural, one {år} other {# år}}" }, "path": "Sti", "pattern": "Mønster", @@ -846,7 +873,7 @@ "shared_by_you": "Delt af dig", "shared_from_partner": "Billeder fra {partner}", "shared_links": "Delte links", - "shared_photos_and_videos_count": "{assetCount} delte billeder & videoer.", + "shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}", "shared_with_partner": "Delt med {partner}", "sharing": "Delte", "sharing_sidebar_description": "Vis et link til deling i sidemenuen", @@ -883,7 +910,7 @@ "stop_photo_sharing": "Stop med at dele dine billeder?", "stop_photo_sharing_description": "{partner} vil ikke længere kunne tilgå dine billeder.", "stop_sharing_photos_with_user": "Afslut deling af dine fotos med denne bruger", - "storage": "Lagring", + "storage": "Lagringsplads", "storage_label": "Lagringsmærkat", "storage_usage": "{used} ud af {available} brugt", "submit": "Indsend", @@ -900,7 +927,7 @@ "to_archive": "Arkivér", "to_favorite": "Gør til favorit", "toggle_settings": "Slå indstillinger til eller fra", - "toggle_theme": "Slå tema til eller fra", + "toggle_theme": "Slå mørkt tema til eller fra", "toggle_visibility": "Slå synlighed til eller fra", "total_usage": "Samlet forbrug", "trash": "Papirkurv", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 927f505936b86..ce51f11b66882 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -139,7 +139,11 @@ "map_settings_description": "Karten- und GPS Einstellungen verwalten", "map_style_description": "URL zu einem style.json Karten-Theme", "metadata_extraction_job": "Metadaten extrahieren", - "metadata_extraction_job_description": "Extrahieren von Metadaten, wie zum Beispiel GPS und Auflösung aus jeder Datei", + "metadata_extraction_job_description": "Extrahieren von Metadaten, wie zum Beispiel GPS, Gesichtern und Auflösung aus jeder Datei", + "metadata_faces_import_setting": "Import von Gesichtern aktivieren", + "metadata_faces_import_setting_description": "Gesichter aus EXIF Daten des Bildes und Sidecar Dateien importieren", + "metadata_settings": "Metadaten Einstellungen", + "metadata_settings_description": "Metadaten Einstellungen verwalten", "migration_job": "Migration", "migration_job_description": "Diese Aufgabe migriert Miniaturansichten für Dateien und Gesichter in die neueste Ordnerstruktur", "no_paths_added": "Keine Pfade hinzugefügt", @@ -1259,6 +1263,7 @@ "to_change_password": "Passwort ändern", "to_favorite": "Zu Favoriten hinzufügen", "to_login": "Anmelden", + "to_parent": "Gehe zum Übergeordneten", "to_root": "Zur Wurzel", "to_trash": "Zum Papierkorb verschieben", "toggle_settings": "Einstellungen umschalten", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index ffdca3f7533ba..509c6d515f25c 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -139,7 +139,11 @@ "map_settings_description": "Administrar la configuración del mapa", "map_style_description": "Dirección URL a un tema de mapa (style.json)", "metadata_extraction_job": "Extracción de metadatos", - "metadata_extraction_job_description": "Extrae información de metadatos de cada elemento, como GPS y resolución", + "metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras y resolución", + "metadata_faces_import_setting": "Activar importación de caras", + "metadata_faces_import_setting_description": "Importar caras desde los metadatos EXIF y auxiliares de una imagen", + "metadata_settings": "Configuración de metadatos", + "metadata_settings_description": "Administrar la configuración de metadatos", "migration_job": "Migración", "migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas más reciente", "no_paths_added": "No se han añadido carpetas", @@ -947,7 +951,7 @@ "options": "Opciones", "or": "o", "organize_your_library": "Organiza tu biblioteca", - "original": "oeiginal", + "original": "original", "other": "Otro", "other_devices": "Otro dispositivo", "other_variables": "Otras variables", @@ -1259,6 +1263,7 @@ "to_change_password": "Cambiar contraseña", "to_favorite": "A los favoritos", "to_login": "Iniciar Sesión", + "to_parent": "Ir a los padres", "to_root": "Para root", "to_trash": "Papelera", "toggle_settings": "Alternar ajustes", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index 7cbf760e06a97..c9fe916c03cca 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -139,7 +139,11 @@ "map_settings_description": "Gérer les paramètres de la carte", "map_style_description": "URL vers un thème de carte au format style.json", "metadata_extraction_job": "Extraction des métadonnées", - "metadata_extraction_job_description": "Extraction des informations des métadonnées de chaque média, telles que la position GPS et la résolution", + "metadata_extraction_job_description": "Extraction des informations des métadonnées de chaque média, telles que la position GPS, les visages et la résolution", + "metadata_faces_import_setting": "Active l'importation des visages", + "metadata_faces_import_setting_description": "Importation de visages à partir des données EXIF des images et de fichiers sidecar", + "metadata_settings": "Paramètres des métadonnées", + "metadata_settings_description": "Gestion des paramètres de métadonnées", "migration_job": "Migration", "migration_job_description": "Migration des miniatures pour les médias et les visages vers la dernière structure de dossiers", "no_paths_added": "Aucun chemin n'a été ajouté", @@ -1259,6 +1263,7 @@ "to_change_password": "Modifier le mot de passe", "to_favorite": "Ajouter aux favoris", "to_login": "Se connecter", + "to_parent": "Aller au dossier parent", "to_root": "Vers la racine", "to_trash": "Corbeille", "toggle_settings": "Inverser les paramètres", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index 44efea31b8085..aefa831897de7 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -1259,6 +1259,7 @@ "to_change_password": "שנה סיסמה", "to_favorite": "מועדף", "to_login": "כניסה", + "to_parent": "לך להורה", "to_root": "לשורש", "to_trash": "אשפה", "toggle_settings": "החלף מצב הגדרות", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 0f1fb75a22082..d6b3373152c63 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -139,7 +139,11 @@ "map_settings_description": "Beheer kaartinstellingen", "map_style_description": "URL naar een style.json kaartthema", "metadata_extraction_job": "Metadata ophalen", - "metadata_extraction_job_description": "Metadata ophalen van iedere asset, zoals GPS en resolutie", + "metadata_extraction_job_description": "Metadata ophalen van iedere asset, zoals GPS, gezichten en resolutie", + "metadata_faces_import_setting": "Gezichten importeren inschakelen", + "metadata_faces_import_setting_description": "Gezichten importeren uit EXIF-gegevens van afbeeldingen en sidecar bestanden", + "metadata_settings": "Metadata instellingen", + "metadata_settings_description": "Beheer metadata instellingen", "migration_job": "Migratie", "migration_job_description": "Migreer thumbnails voor assets en gezichten naar de nieuwste mapstructuur", "no_paths_added": "Geen paden toegevoegd", @@ -1209,6 +1213,8 @@ "sign_up": "Registreren", "size": "Grootte", "skip_to_content": "Doorgaan naar inhoud", + "skip_to_folders": "Doorgaan naar mappen", + "skip_to_tags": "Doorgaan naar tags", "slideshow": "Diavoorstelling", "slideshow_settings": "Diavoorstelling instellingen", "sort_albums_by": "Sorteer albums op...", @@ -1260,6 +1266,7 @@ "to_change_password": "Wijzig wachtwoord", "to_favorite": "Toevoegen aan favorieten", "to_login": "Inloggen", + "to_parent": "Ga naar hoofdmap", "to_root": "Naar hoofdmap", "to_trash": "Prullenbak", "toggle_settings": "Zichtbaarheid instellingen wisselen", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index fead1913da326..534794b6d312c 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -139,7 +139,11 @@ "map_settings_description": "Gestionare setări hartă", "map_style_description": "URL-ul style.json către o temă pentru hartă", "metadata_extraction_job": "Extragere metadata", - "metadata_extraction_job_description": "Extragere informații metadata din fiecare fișier cum ar fi localizare GPS, rezoluție, etc", + "metadata_extraction_job_description": "Extragere informații metadata din fiecare fișier cum ar fi localizare GPS, fețe și rezoluție,", + "metadata_faces_import_setting": "Activare import fețe", + "metadata_faces_import_setting_description": "Importă fețe din datele EXIF ale imaginii și din fișiere tip \"sidecar\"", + "metadata_settings": "Setări Metadata", + "metadata_settings_description": "Gestionează setările metadata", "migration_job": "Migrare", "migration_job_description": "Migrați miniaturile pentru elemente și fețe la cea mai recentă structură de foldere", "no_paths_added": "Nicio cale adăugată", @@ -219,14 +223,18 @@ "slideshow_duration_description": "Numǎrul de secunde pentru afișarea fiecǎrei imagini", "smart_search_job_description": "Rulați machine learning pe active pentru a sprijini căutarea inteligentă", "storage_template_date_time_description": "Momentul creării activului este utilizat pentru informațiile privind data și ora", + "storage_template_date_time_sample": "Eșantion de timp {date}", "storage_template_enable_description": "Activați motorul de șabloane de stocare", "storage_template_hash_verification_enabled": "Verificarea hash este activată", "storage_template_hash_verification_enabled_description": "Activează verificarea hash, nu o dezactivați decât dacă sunteți sigur de implicații", "storage_template_migration": "Migrarea șablonului de stocare", "storage_template_migration_description": "Aplicați {template} actual la elementele încărcate anterior", "storage_template_migration_info": "Modificările de șablon se vor aplica numai materialelor noi. Pentru a aplica retroactiv șablonul la materialele încărcate anterior, rulați {job}.", - "storage_template_migration_job": "", - "storage_template_settings": "", + "storage_template_migration_job": "Activitate migrare template stocare", + "storage_template_more_details": "Pentru mai multe detalii despre aceasta caracteristică, accesați Șablon stocare si implicațiile", + "storage_template_onboarding_description": "Atunci când este activată, această caracteristică va organiza automat fișierele pe baza unui șablon definit de utilizator. Din cauza unor probleme de stabilitate, aceasta caracteristică este dezactivată implicit. Pentru mai multe informații, te rog sa consulți documentația.", + "storage_template_path_length": "Limita de lungime pentru calea aproximativă: {length, number}/{limit, number}", + "storage_template_settings": "Șablon stocare", "storage_template_settings_description": "", "system_settings": "Setǎri de sistem", "theme_custom_css_settings": "CSS personalizat", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index f0389598e79a1..6d28e90db0ea1 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -140,6 +140,10 @@ "map_style_description": "URL-адрес темы карты style.json", "metadata_extraction_job": "Извлечение метаданных", "metadata_extraction_job_description": "Извлекает метаданные из каждого ресурса, такие как координаты GPS и разрешение", + "metadata_faces_import_setting": "Включить импорт лиц", + "metadata_faces_import_setting_description": "Импорт лиц из изображений EXIF-данных и файлов sidecar", + "metadata_settings": "Настройки метаданных", + "metadata_settings_description": "Управление настройками метаданных", "migration_job": "Миграция", "migration_job_description": "Выполняет перенос миниатюр для ресурсов и лиц в последнюю структуру папок", "no_paths_added": "Пути не добавлены", @@ -1208,6 +1212,8 @@ "sign_up": "Войти", "size": "Размер", "skip_to_content": "Перейти к содержанию", + "skip_to_folders": "Перейти к папкам", + "skip_to_tags": "Перейти к тегам", "slideshow": "Слайд-шоу", "slideshow_settings": "Настройки слайд-шоу", "sort_albums_by": "Сортировать альбомы по...", @@ -1218,7 +1224,7 @@ "sort_recent": "Недавние фото", "sort_title": "Заголовок", "source": "Источник", - "stack": "Стек", + "stack": "В стопку", "stack_duplicates": "Стек дубликатов", "stack_select_one_photo": "Выберите одну главную фотографию для стека", "stack_selected_photos": "Сложить выбранные фотографии в стопку", @@ -1259,6 +1265,7 @@ "to_change_password": "Изменить пароль", "to_favorite": "Добавить в избранное", "to_login": "Вход", + "to_parent": "Вернуться назад", "to_root": "В начало", "to_trash": "Корзина", "toggle_settings": "Переключение настроек", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index e1e548b242d75..d3569146d7209 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -1259,6 +1259,7 @@ "to_change_password": "Промени лозинку", "to_favorite": "Постави као фаворит", "to_login": "Пријава", + "to_parent": "Врати се назад", "to_root": "На почетак", "to_trash": "Смеће", "toggle_settings": "Намести подешавања", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index defe244060d32..8b0c53ded0a88 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -1259,6 +1259,7 @@ "to_change_password": "Promeni lozinku", "to_favorite": "Postavi kao favorit", "to_login": "Prijava", + "to_parent": "Vrati se nazad", "to_root": "Na početak", "to_trash": "Smeće", "toggle_settings": "Namesti podešavanja", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index c7b153d96b2c5..2a02ac7138f7f 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -33,7 +33,7 @@ "authentication_settings_disable_all": "Är du säker på att du vill inaktivera alla inloggningsmetoder? Inloggning kommer att helt inaktiveras.", "authentication_settings_reenable": "För att återaktivera, använd Server Command.", "background_task_job": "Bakgrundsaktiviteter", - "check_all": "Välj Alla", + "check_all": "Välj alla", "cleared_jobs": "Rensade jobben för:{job}", "config_set_by_file": "Konfigurationen är satt av en konfigurationsfil", "confirm_delete_library": "Är du säker på att du vill radera {library} album?", @@ -44,12 +44,12 @@ "crontab_guru": "Crontab-guru", "disable_login": "Inaktivera inloggning", "disabled": "Inaktiverad", - "duplicate_detection_job_description": "Kör maskininlärning på tillgångar för att upptäcka liknande bilder. Förlitar sig på Smart Search", + "duplicate_detection_job_description": "Kör maskininlärning på objekt för att upptäcka liknande bilder. Bygger på Smart Search", "exclusion_pattern_description": "Exkluderingsmönster tillåter dig att ignorera filer och mappar när skanning görs av ditt album. Detta är användbart om du har mappar som innehåller filer som du inte vill importera, t.ex. RAW-filer.", "external_library_created_at": "Externt bibliotek (skapat den {date})", - "external_library_management": "Extern bibliotekshantering", + "external_library_management": "Hantera externa bibliotek", "face_detection": "Ansiktsdetektering", - "face_detection_description": "Upptäck ansikten i mediafilerna med hjälp av maskininlärning. För videor används endast miniatyren. \"Alla\" (om)bearbetar alla tillgångar. \"Saknade\" kötillgångar som inte har bearbetats ännu. Upptäckta ansikten ställs i kö för ansiktsigenkänning efter att ansiktsigenkänning är klar, och grupperar dem i befintliga eller nya personer.", + "face_detection_description": "Identifiera ansikten i foton med hjälp av maskininlärning. För videor används endast miniatyrbilden. \"Alla\" gör om sökningen för alla objekt. \"Saknade\" letar i de objekt som ännu inte sökts igenom. Alla ansikten som identifierats läggs sedan i jobbkön för ansiktsigenkänning där de mappas till nya eller befintliga personer.", "facial_recognition_job_description": "Gruppera upptäckta ansikten till personer. Det här steget körs efter att ansiktsigenkänning är klar. \"Alla\" (åter-) grupperar alla ansikten. \"Saknade\" köer ansikten som inte har en person tilldelad.", "failed_job_command": "Kommando {command} misslyckades för jobb: {job}", "force_delete_user_warning": "VARNING: Detta tar omedelbart bort användaren och alla mediafiler. Detta kan inte ångras och filerna kan inte återställas.", @@ -85,9 +85,9 @@ "library_scanning": "Periodisk skanning", "library_scanning_description": "Konfigurera periodisk biblioteksskanning", "library_scanning_enable_description": "Aktivera periodisk biblioteksskanning", - "library_settings": "Externt bibliotek", - "library_settings_description": "Hantera inställningar för externt bibliotek", - "library_tasks_description": "Utför biblioteksuppgifter", + "library_settings": "Externa bibliotek", + "library_settings_description": "Hantera inställningar för externa bibliotek", + "library_tasks_description": "Kör biblioteksjobb", "library_watching_enable_description": "Titta på externa bibliotek för filändringar", "library_watching_settings": "Titta på bibliotek (EXPERIMENTELLT)", "library_watching_settings_description": "Titta automatiskt efter ändrade filer", @@ -139,7 +139,11 @@ "map_settings_description": "Hantera kartinställningar", "map_style_description": "URL till en style.json-karto tema", "metadata_extraction_job": "Extrahera metadata", - "metadata_extraction_job_description": "Extrahera metadata-information from varje resurs, så som GPS och upplösning", + "metadata_extraction_job_description": "Läs in metadata (t.ex. GPS, ansikten och upplösning) för varje resurs", + "metadata_faces_import_setting": "Aktivera import av ansikten", + "metadata_faces_import_setting_description": "Importera ansikten från bildens EXIF-data och sidecar-fil", + "metadata_settings": "Metadata-inställningar", + "metadata_settings_description": "Hantera metadata-inställningar", "migration_job": "Migrering", "migration_job_description": "Migrera miniatyrbilder för resurser och ansikten till den senaste mappstrukturen", "no_paths_added": "Inga vägar tillagda", @@ -191,7 +195,7 @@ "offline_paths": "Offline-sökvägar", "offline_paths_description": "Dessa resultat kan bero på manuell borttagning av filer som inte är en del av ett externt bibliotek.", "password_enable_description": "Logga in med epost och lösenord", - "password_settings": "Lösenords-inloggning", + "password_settings": "Lösenordsinloggning", "password_settings_description": "Hantera inställningar för lösenords-inloggning", "paths_validated_successfully": "Samtliga sökvägar kunde bekräftas", "quota_size_gib": "Lagringskvot (GiB)", @@ -315,7 +319,7 @@ }, "admin_email": "", "admin_password": "", - "administration": "Administrering", + "administration": "Administration", "advanced": "Avancerad", "album_added": "", "album_added_notification_setting_description": "", @@ -749,7 +753,7 @@ "saved_profile": "", "saved_settings": "", "say_something": "Säg något", - "scan_all_libraries": "", + "scan_all_libraries": "Skanna alla bibliotek", "scan_all_library_files": "", "scan_new_library_files": "", "scan_settings": "", @@ -795,7 +799,7 @@ "shared_by": "", "shared_by_you": "", "shared_links": "Delade Länkar", - "sharing": "Delas", + "sharing": "Delning", "sharing_sidebar_description": "", "show_album_options": "", "show_file_location": "", @@ -827,7 +831,7 @@ "status": "Status", "stop_motion_photo": "", "stop_photo_sharing": "Sluta dela dina foton?", - "storage": "Lagringsutrymme", + "storage": "Lagring", "storage_label": "", "storage_usage": "{used} av {available} används", "submit": "", @@ -852,7 +856,7 @@ "trash": "Papperskorg", "trash_all": "", "trash_no_results_message": "Borttagna foton och videor kommer att visas här.", - "trashed_items_will_be_permanently_deleted_after": "Borttagna objekt kommer att tas bort permanent efter {days, plural, one {# dag} other {# dagar}}.", + "trashed_items_will_be_permanently_deleted_after": "Objekt i papperskorgen raderas permanent efter {days, plural, one {# dag} other {# dagar}}.", "type": "Typ", "unarchive": "Ångra arkivering", "unarchived": "", diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 62966fe8a7d0c..4fefbf2f2132a 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -134,13 +134,17 @@ "map_reverse_geocoding": "Coğrafi Kodlama", "map_reverse_geocoding_enable_description": "Coğrafi Kodlamayı etkinleştir", "map_reverse_geocoding_settings": "Coğrafi Kodlama ayarları", - "map_settings": "Harita ayarları", + "map_settings": "Harita", "map_settings_description": "Harita ayarlarını yönet", "map_style_description": "style.json Harita ayarlarının URL'si", "metadata_extraction_job": "Metadata'yı çıkart", "metadata_extraction_job_description": "Tüm varlıklardan GPS, çözünürlük gibi metadatayı çıkart", + "metadata_faces_import_setting": "Yüzleri alma aktif", + "metadata_faces_import_setting_description": "Yüzleri, EXIF verileri ve sidecar dosyalardan getir", + "metadata_settings": "Metaveri Ayarları", + "metadata_settings_description": "Metaveri ayarlarını yönet", "migration_job": "Birleştirme", - "migration_job_description": "Varlık önizlemelerini en yeni klasör yapısına aktar", + "migration_job_description": "Varlıklar ve yüzler için resim çerçeve önizlemelerini en yeni klasör yapısına aktar", "no_paths_added": "Yol eklenmedi", "no_pattern_added": "Desen eklenmedi", "note_apply_storage_label_previous_assets": "Not: Depolama adresini daha önce yüklenmiş dosyalara uygulamak için", @@ -173,7 +177,7 @@ "oauth_issuer_url": "Yayınlayıcı URL", "oauth_mobile_redirect_uri": "Mobil yönlendirme URL'si", "oauth_mobile_redirect_uri_override": "Mobilde zorla kullanılacak Yönlendirme Adresi", - "oauth_mobile_redirect_uri_override_description": "'app.immich:/' URL'si geçersiz olduğunda etkinleştir.", + "oauth_mobile_redirect_uri_override_description": "OAuth sağlayıcısı '{callback}'gibi bir mobil URI'ye izin vermediğinde etkinleştir.", "oauth_profile_signing_algorithm": "Profil imzalama algoritması", "oauth_profile_signing_algorithm_description": "Kullanıcının profilini imzalarken kullanılacak güvenlik algoritması.", "oauth_scope": "Kapsam", @@ -199,7 +203,7 @@ "registration_description": "Sistemdeki ilk kullanıcı olduğunuz için hesabınız Yönetici olarak ayarlandı. Yeni oluşturulan üyeliklerin, ve yönetici görevlerinin sorumlusu olarak atandınız.", "removing_offline_files": "Çevrimdışı dosyalar kaldırılıyor", "repair_all": "Tümünü onar", - "repair_matched_items": "", + "repair_matched_items": "Eşleşen {sayı, çoğul, bir {# öğe} diğer {# öğeler}}", "repaired_items": "{count, plural, one {# item} other {# items}} tamir edildi", "require_password_change_on_login": "Kullanıcının ilk girişinde şifre değiştirmesini zorunlu kıl", "reset_settings_to_default": "Ayarları varsayılana sıfırla", @@ -271,15 +275,15 @@ "transcoding_max_b_frames_description": "Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. Eski cihazlarda donanım hızlandırma ile uyumlu olmayabilir. 0, B-çerçevelerini devre dışı bırakır, -1 ise bu değeri otomatik olarak ayarlar.", "transcoding_max_bitrate": "Maksimum bitrate", "transcoding_max_bitrate_description": "Maksimum bit hızı ayarlamak, kaliteye küçük bir maliyetle dosya boyutlarını daha öngörülebilir hale getirebilir.", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", + "transcoding_max_keyframe_interval": "Maksimum ana kare aralığı", + "transcoding_max_keyframe_interval_description": "Ana kareler arasındaki maksimum kare mesafesini ayarlar. Düşük değerler sıkıştırma verimliliğini kötüleştirir, ancak arama sürelerini iyileştirir ve hızlı hareket içeren sahnelerde kaliteyi artırabilir. 0 bu değeri otomatik olarak ayarlar.", "transcoding_optimal_description": "Hedef çözünürlükten yüksek veya kabul edilen formatta olmayan videolar", "transcoding_preferred_hardware_device": "Tercih edilen donanım cihazı", "transcoding_preferred_hardware_device_description": "Sadece VAAPI ve QSV için uygulanır. Donanım kod çevrimi için DRI Node ayarlar.", "transcoding_preset_preset": "", "transcoding_preset_preset_description": "Sıkıştırma hızı. Daha yavaş olan ayarlar belirli bitrate ayarları için daha küçük ve daha kaliteli dosya üretir. VP9 ayarı 'daha hızlı' ayarının üstündeki ayarları görmezden gelir.", "transcoding_reference_frames": "Referans kareler", - "transcoding_reference_frames_description": "", + "transcoding_reference_frames_description": "Belirli bir kareyi sıkıştırırken referans alınacak kare sayısı. Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. 0 bu değeri otomatik olarak ayarlar.", "transcoding_required_description": "Yalnızca kabul edilen formatta olmayan videolar", "transcoding_settings": "Video Dönüştürme Ayarları", "transcoding_settings_description": "Video dosyalarının çözünürlük ve kodlama bilgilerini yönetir", @@ -288,17 +292,17 @@ "transcoding_temporal_aq": "", "transcoding_temporal_aq_description": "Sadece NVENC için geçerlidir. Yüksek-detayların ve düşük-hareket sahnelerin kalitesini arttır. Eski cihazlarla uyumlu olmayabilir.", "transcoding_threads": "İş Parçacıkları", - "transcoding_threads_description": "", + "transcoding_threads_description": "Daha yüksek değerler daha hızlı kodlamaya yol açar, ancak sunucunun etkin durumdayken diğer görevleri işlemesi için daha az alan bırakır. Bu değer İşlemci çekirdeği sayısından fazla olmamalıdır. 0'a ayarlanırsa kullanımı en üst düzeye çıkarır.", "transcoding_tone_mapping": "Ton-haritalama", "transcoding_tone_mapping_description": "HDR videoların SDR'ye dönüştürülürken görünümünü korumayı amaçlar. Her algoritma renk, detay ve parlaklık için farklı dengeleme yapar. Hable detayları korur, Mobius renkleri korur ve Reinhard parlaklığı korur.", "transcoding_tone_mapping_npl": "", "transcoding_tone_mapping_npl_description": "Renkler, bu parlaklıkta bir ekran için normal görünecek şekilde ayarlanacaktır. Karşıt olarak, daha düşük değerler videonun parlaklığını artırır ve tersi de geçerlidir çünkü ekranın parlaklığını telafi eder. 0 bu değeri otomatik olarak ayarlar.", - "transcoding_transcode_policy": "", - "transcoding_transcode_policy_description": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", + "transcoding_transcode_policy": "Dönüştürme(çevirme) politikası", + "transcoding_transcode_policy_description": "Bir videonun ne zaman kod dönüştürülmesi gerektiğine ilişkin ilke. Dönüştürme devre dışı bırakılmadığı sürece HDR videolar her zaman dönüştürülür.", + "transcoding_two_pass_encoding": "İki geçişli kodlama", + "transcoding_two_pass_encoding_setting_description": "Daha iyi kodlanmış videolar üretmek için iki geçişte kod dönüştürün. Maksimum bit hızı etkinleştirildiğinde (H.264 ve HEVC ile çalışması için gereklidir), bu mod maksimum bit hızına dayalı bir bit hızı aralığı kullanır ve CRF'yi yok sayar. VP9 için, maksimum bit hızı devre dışı bırakılırsa CRF kullanılabilir.", "transcoding_video_codec": "Video kodek", - "transcoding_video_codec_description": "", + "transcoding_video_codec_description": "VP9 yüksek verimliliğe ve web uyumluluğuna sahiptir, ancak kod dönüştürme işlemi daha uzun sürer. HEVC benzer performans gösterir ancak web uyumluluğu daha düşüktür. H.264 geniş çapta uyumludur ve kod dönüştürmesi hızlıdır, ancak çok daha büyük dosyalar üretir. AV1 en verimli codec'tir ancak eski cihazlarda desteği yoktur.", "trash_enabled_description": "Çöp özelliklerini etkinleştir", "trash_number_of_days": "Gün sayısı", "trash_number_of_days_description": "Varlıkların kalıcı olarak silinmeden önce çöpte kaç gün tutulacağı", @@ -308,20 +312,22 @@ "untracked_files_description": "Bu dosyalar uygulama tarafından izlenmiyor. Yarıda kesilen yüklemeler veya uygulama hatası bunlara sebep olmuş olabilir", "user_delete_delay": "{user} hesabı ve varlıkları {delay, plural, one {# day} other {# days}} gün içinde kalıcı olarak silinmek için planlandı.", "user_delete_delay_settings": "Silme gecikmesi", - "user_delete_delay_settings_description": "", + "user_delete_delay_settings_description": "Bir kullanıcının hesabını ve varlıklarını kalıcı olarak silmek için kaldırıldıktan sonra gereken gün sayısı. Kullanıcı silme işi, silinmeye hazır kullanıcıları kontrol etmek için gece yarısı çalışır. Bu ayardaki değişiklikler bir sonraki yürütmede değerlendirilecektir.", + "user_delete_immediately": "{Kullanıcı}'nın hesabı ve varlıkları hemen kalıcı olarak silinmek üzere sıraya alınacak.", "user_delete_immediately_checkbox": "Kullanıcıyı ve tüm varlıklarını kalıcı olarak silmek için sıraya koy", "user_management": "Kullanıcı Yönetimi", "user_password_has_been_reset": "Kullanıcının şifresi sıfırlandı:", - "user_password_reset_description": "", + "user_password_reset_description": "Lütfen kullanıcıya geçici şifreyi sağlayın ve bir sonraki oturum açışında şifreyi değiştirmesi gerektiğini bildirin.", "user_restore_description": "{user} kullanıcısı geri yüklenecek.", + "user_restore_scheduled_removal": "Kullanıcıyı geri yükle - {tarih, tarih, uzun} tarihinde zamanlanmış kaldırma", "user_settings": "Kullanıcı Ayarları", "user_settings_description": "Kullanıcı Ayarlarını Yönet", "user_successfully_removed": "Kullanıcı {email} başarıyla kaldırıldı.", - "version_check_enabled_description": "", + "version_check_enabled_description": "Sürüm kontrolü etkin", "version_check_settings": "Versiyon kontrolü", "version_check_settings_description": "Yeni sürüm bildirimini etkinleştir/devre dışı bırak", "video_conversion_job": "", - "video_conversion_job_description": "" + "video_conversion_job_description": "Tarayıcılar ve cihazlarla daha geniş uyumluluk için videoları dönüştür" }, "admin_email": "Yönetici Emaili", "admin_password": "Yönetici Şifresi", @@ -339,7 +345,7 @@ "album_options": "Albüm seçenekleri", "album_remove_user": "Kullanıcıyı kaldır?", "album_remove_user_confirmation": "{user} kullanıcısını kaldırmak istediğinize emin misiniz?", - "album_share_no_users": "Bu albümü tüm kullanıcılarla paylaşmışsınız ya da paylaşacak kullanıcı bulunmuyor.", + "album_share_no_users": "Görünüşe göre bu albümü tüm kullanıcılarla paylaştınız veya paylaşacak herhangi bir başka kullanıcınız yok.", "album_updated": "Albüm güncellendi", "album_updated_setting_description": "Paylaşılan bir albüme yeni bir varlık eklendiğinde email bildirimi alın", "album_user_left": "{album}den ayrıldınız", @@ -353,6 +359,9 @@ "all_videos": "Tüm Videolar", "allow_dark_mode": "Koyu moda izin ver", "allow_edits": "Düzenlemeye izin ver", + "allow_public_user_to_download": "Genel kullanıcının indirmesine aç", + "allow_public_user_to_upload": "Genel kullanıcının yüklemesine aç", + "anti_clockwise": "Saat yönünün tersine", "api_key": "API Anahtarı", "api_key_description": "Bu değer sadece bir kere gösterilecek. Lütfen bu pencereyi kapatmadan önce kopyaladığınıza emin olun.", "api_key_empty": "Apı Anahtarı isminiz boş olmamalı", @@ -372,6 +381,7 @@ "asset_filename_is_offline": "Varlık {filename} çevrimdışı", "asset_has_unassigned_faces": "Varlık, atanmamış yüzler içeriyor", "asset_offline": "Varlık çevrimdışı", + "asset_offline_description": "Bu varlık çevrimdışı. Immich dosya konumuna erişemiyor. Lütfen varlığın kullanılabilir olduğundan emin olun ve ardından kitaplığı yeniden tarayın.", "asset_skipped": "Atlandı", "asset_uploaded": "Yüklendi", "asset_uploading": "Yükleniyor...", @@ -384,6 +394,10 @@ "birthdate_saved": "Doğum günü başarılı bir şekilde kaydedildi", "birthdate_set_description": "Doğum günü, fotoğraftaki insanın fotoğraf çekildiği zamandaki yaşının hesaplanması için kullanılır.", "blurred_background": "Bulanık arkaplan", + "bulk_delete_duplicates_confirmation": "Toplu olarak {sayım, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar} 'ı silmek istediğinizden emin misiniz? Bu, her grubun en büyük varlığını tutacak ve diğer tüm kopyaları kalıcı olarak silecektir. Bu işlemi geri alamazsın!", + "bulk_keep_duplicates_confirmation": "{sayım, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar}}ı tutmak istediğinizden emin misiniz? Bu, hiçbir şeyi silmeden tüm yinelenen grupları çözecektir.", + "bulk_trash_duplicates_confirmation": "Toplu olarak {say, çoğul, bir {# yinelenen varlık} diğer {# yinelenen varlıklar} öğesini çöpe atmak istediğinizden emin misiniz? Bu, her grubun en büyük varlığını tutacak ve diğer tüm kopyaları çöpe atacaktır.", + "buy": "Immich'i Satın Alın", "camera": "Kamera", "camera_brand": "Kamera markası", "camera_model": "Kamera modeli", @@ -399,20 +413,22 @@ "change_date": "Tarihi değiştir", "change_expiration_time": "", "change_location": "Konumu değiştir", - "change_name": "İsmi değiştir", - "change_name_successfully": "", + "change_name": "İsim değiştir", + "change_name_successfully": "İsim başarıyla değiştirildi", "change_password": "Şifre Değiştir", "change_password_description": "Bu ya sistemdeki ilk oturum açışınız ya da şifre değişikliği için bir talepte bulunuldu. Lütfen yeni şifreyi aşağıya yazınız.", - "change_your_password": "", - "changed_visibility_successfully": "", + "change_your_password": "Şifreni değiştir", + "changed_visibility_successfully": "Görünürlük başarıyla değiştirildi", "check_all": "", - "check_logs": "", + "check_logs": "Logları Konrol et", "choose_matching_people_to_merge": "Birleştirmek için eşleşen kişileri seçiniz", "city": "Şehir", - "clear": "", + "clear": "Temiz", "clear_all": "Hepsini temizle", - "clear_message": "", - "clear_value": "", + "clear_all_recent_searches": "Son aramaların hepsini temizle", + "clear_message": "Mesajı Temizle", + "clear_value": "Değeri Temizle", + "clockwise": "Saat yönü", "close": "Kapat", "collapse_all": "", "color": "Renk", @@ -435,7 +451,7 @@ "copy_image": "Resmi Kopyala", "copy_link": "Bağlantıyı kopyala", "copy_link_to_clipboard": "Bağlantıyı panoya kopyala", - "copy_password": "", + "copy_password": "Parolayı kopyala", "copy_to_clipboard": "Panoya Kopyala", "country": "Ülke", "cover": "", @@ -446,6 +462,7 @@ "create_link": "Link oluştur", "create_link_to_share": "Paylaşmak için link oluştur", "create_new_person": "Yeni kişi oluştur", + "create_new_person_hint": "Seçili varlıkları yeni bir kişiye atayın", "create_new_user": "Yeni kullanıcı oluştur", "create_tag": "Etiket oluştur", "create_user": "Kullanıcı oluştur", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index fe08e6493a42f..1d5fe69dd325d 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -1257,6 +1257,7 @@ "to_change_password": "Змінити пароль", "to_favorite": "Обране", "to_login": "Вхід", + "to_parent": "Повернутись назад", "to_root": "На початок", "to_trash": "Смітник", "toggle_settings": "Перемикання налаштувань", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index 65c73c09e7b55..4192e7e5f06a7 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -138,8 +138,12 @@ "map_settings": "Bản đồ", "map_settings_description": "Quản lý cài đặt bản đồ", "map_style_description": "Đường dẫn URL đến tập tin tuỳ biến bản đồ style.json", - "metadata_extraction_job": "Trích xuất metadata", - "metadata_extraction_job_description": "Trích xuất metadata từ mỗi ảnh, chẳng hạn như GPS và độ phân giải", + "metadata_extraction_job": "Trích xuất siêu dữ liệu", + "metadata_extraction_job_description": "Trích xuất siêu dữ liệu từ mỗi ảnh, chẳng hạn như GPS, khuôn mặt và độ phân giải", + "metadata_faces_import_setting": "Bật tính năng nhập khuôn mặt", + "metadata_faces_import_setting_description": "Nhập khuôn mặt từ dữ liệu EXIF hình ảnh và tập tin đi kèm", + "metadata_settings": "Siêu dữ liệu", + "metadata_settings_description": "Quản lý cài đặt siêu dữ liệu", "migration_job": "Di chuyển dữ liệu", "migration_job_description": "Di chuyển hình thu nhỏ của các ảnh và khuôn mặt sang cấu trúc thư mục mới", "no_paths_added": "Không có đường dẫn nào được thêm vào", @@ -214,8 +218,8 @@ "server_settings_description": "Quản lý cài đặt máy chủ", "server_welcome_message": "Thông điệp chào mừng", "server_welcome_message_description": "Thông điệp chào mừng được hiển thị trên trang đăng nhập.", - "sidecar_job": "Metadata đi kèm", - "sidecar_job_description": "Tìm hoặc đồng bộ các tập tin metadata đi kèm từ hệ thống", + "sidecar_job": "Siêu dữ liệu đi kèm", + "sidecar_job_description": "Tìm hoặc đồng bộ các tập tin siêu dữ liệu đi kèm từ hệ thống", "slideshow_duration_description": "Số giây để hiển thị cho từng ảnh", "smart_search_job_description": "Chạy machine learning trên toàn bộ ảnh để hỗ trợ tìm kiếm thông minh", "storage_template_date_time_description": "Dấu thời gian tạo ảnh được sử dụng cho thông tin ngày giờ", @@ -583,7 +587,7 @@ "cant_apply_changes": "Không thể áp dụng thay đổi", "cant_change_activity": "Không thể {enabled, select, true {disable} other {enable}} hoạt động", "cant_change_asset_favorite": "Không thể thay đổi yêu thích cho ảnh", - "cant_change_metadata_assets_count": "Không thể thay đổi metadata của {count, plural, one {# mục} other {# mục}}", + "cant_change_metadata_assets_count": "Không thể thay đổi siêu dữ liệu của {count, plural, one {# mục} other {# mục}}", "cant_get_faces": "Không thể tải khuôn mặt", "cant_get_number_of_comments": "Không thể tải số lượng bình luận", "cant_search_people": "Không thể tìm kiếm người", @@ -1032,12 +1036,12 @@ "recent_searches": "Tìm kiếm gần đây", "refresh": "Làm mới", "refresh_encoded_videos": "Làm mới video đã mã hóa", - "refresh_metadata": "Làm mới metadata", + "refresh_metadata": "Làm mới siêu dữ liệu", "refresh_thumbnails": "Làm mới hình thu nhỏ", "refreshed": "Đã làm mới", "refreshes_every_file": "Làm mới mọi tập tin", "refreshing_encoded_video": "Đang làm mới video đã mã hóa", - "refreshing_metadata": "Đang làm mới metadata", + "refreshing_metadata": "Đang làm mới siêu dữ liệu", "regenerating_thumbnails": "Đang tạo lại hình thu nhỏ", "remove": "Xóa", "remove_assets_album_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi album?", @@ -1163,7 +1167,7 @@ "show_in_timeline": "Hiển thị trên dòng thời gian", "show_in_timeline_setting_description": "Hiển thị ảnh và video từ người dùng này trong dòng thời gian của bạn", "show_keyboard_shortcuts": "Hiển thị phím tắt", - "show_metadata": "Hiển thị metadata", + "show_metadata": "Hiển thị siêu dữ liệu", "show_or_hide_info": "Hiển thị hoặc ẩn thông tin", "show_password": "Hiển thị mật khẩu", "show_person_options": "Hiển thị tùy chọn người", @@ -1178,6 +1182,8 @@ "sign_up": "Đăng ký", "size": "Kích thước", "skip_to_content": "Bỏ qua nội dung", + "skip_to_folders": "Chuyển đến thư mục", + "skip_to_tags": "Chuyển đến thẻ", "slideshow": "Trình chiếu", "slideshow_settings": "Cài đặt trình chiếu", "sort_albums_by": "Sắp xếp album theo...", @@ -1229,6 +1235,7 @@ "to_change_password": "Đổi mật khẩu", "to_favorite": "Yêu thích", "to_login": "Đăng nhập", + "to_parent": "Đến thư mục cha", "to_root": "Tới thư mục gốc", "to_trash": "Xóa", "toggle_settings": "Chuyển đổi cài đặt", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index 4aa7338f802c7..afe01754c16b1 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -140,6 +140,10 @@ "map_style_description": "地图主题 style.json 的 URL", "metadata_extraction_job": "提取元数据", "metadata_extraction_job_description": "从每个项目中提取元数据信息,如GPS和分辨率", + "metadata_faces_import_setting": "启用人脸导入", + "metadata_faces_import_setting_description": "从图片的EXIF和辅助元数据中导入人脸", + "metadata_settings": "元数据设置", + "metadata_settings_description": "管理元数据设置", "migration_job": "迁移", "migration_job_description": "将项目和人脸识别的缩略图迁移到最新的文件夹结构", "no_paths_added": "无已添加路径", @@ -298,7 +302,7 @@ "transcoding_transcode_policy": "转码策略", "transcoding_transcode_policy_description": "视频转码策略。HDR视频将始终进行转码(除非禁用了转码功能)。", "transcoding_two_pass_encoding": "二次编码", - "transcoding_two_pass_encoding_setting_description": "分两次进行转码,以生成更好的编码视频。当启用最大比特率(与H.264和HEVC一起工作所需)时,此模式使用基于最大比特率的比特率范围,并忽略CRF。对于VP9,如果禁用了最大比特率,则可以使用CRF。\n注:CRF,全称为constant rate factor,是指保证“一定质量”,智能分配码率,包括同一帧内分配码率、帧间分配码率。", + "transcoding_two_pass_encoding_setting_description": "分两次进行转码,以生成更好的编码视频。当启用最大比特率(与H.264和HEVC一起工作所需)时,此模式使用基于最大比特率的比特率范围,并忽略CRF。对于VP9,如果禁用了最大比特率,则可以使用CRF(注:CRF,全称为constant rate factor,是指保证“一定质量”,智能分配码率,包括同一帧内分配码率、帧间分配码率)。", "transcoding_video_codec": "视频编解码器", "transcoding_video_codec_description": "VP9具有很高的效率和网络兼容性,但转码需要更长的时间。HEVC的性能相似,但网络兼容性较低。H.264转码快速且具有广泛的兼容性,但产生的文件要大得多。AV1是最高效的编解码器,但在较旧的设备上缺乏支持。", "trash_enabled_description": "启用回收站", @@ -1182,7 +1186,7 @@ "sharing": "共享", "sharing_enter_password": "请输入密码后查看此页面。", "sharing_sidebar_description": "在侧边栏中显示共享链接", - "shift_to_permanent_delete": "按住⇧永久删除项目", + "shift_to_permanent_delete": "按住Shift键永久删除项目", "show_album_options": "显示相册选项", "show_albums": "显示相册", "show_all_people": "显示所有人物", @@ -1208,6 +1212,8 @@ "sign_up": "注册", "size": "大小", "skip_to_content": "跳到内容", + "skip_to_folders": "跳到文件夹", + "skip_to_tags": "跳到标签", "slideshow": "幻灯片放映", "slideshow_settings": "放映设置", "sort_albums_by": "相册排序依据...", @@ -1259,6 +1265,7 @@ "to_change_password": "修改密码", "to_favorite": "收藏", "to_login": "登录", + "to_parent": "返回上一级", "to_root": "返回到根目录", "to_trash": "放入回收站", "toggle_settings": "切换设置", From aa0097bde2cca98dc66db25e58ec5c1c1212ccfb Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:30:34 -0400 Subject: [PATCH 05/26] fix(server): copy video projection metadata for 360 videos (#12376) --- server/src/services/media.service.spec.ts | 5 +++++ server/src/utils/media.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 634cd790ebd0f..bf493de0f39d1 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1247,6 +1247,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-v verbose', '-vf scale=-2:720,format=yuv420p', '-preset 12', @@ -1372,6 +1373,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-g 256', '-v verbose', '-vf format=nv12,hwupload_cuda,scale_cuda=-2:720', @@ -1532,6 +1534,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-bf 7', '-refs 5', '-g 256', @@ -1716,6 +1719,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-g 256', '-v verbose', '-vf format=nv12,hwupload,scale_vaapi=-2:720', @@ -1913,6 +1917,7 @@ describe(MediaService.name, () => { '-fps_mode passthrough', '-map 0:0', '-map 0:1', + '-strict unofficial', '-g 256', '-v verbose', '-vf scale_rkrga=-2:720:format=nv12:afbc=1', diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index cf8e438349e52..8068f4a5e6587 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -115,6 +115,7 @@ export class BaseConfig implements VideoCodecSWConfig { '-fps_mode passthrough', // explicitly selects the video stream instead of leaving it up to FFmpeg `-map 0:${videoStream.index}`, + '-strict unofficial', ]; if (audioStream) { From 9fc30d6bf633b906590b8662f71b831f211411a0 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:15:48 +0200 Subject: [PATCH 06/26] fix(web): auth on navigation from shared link to timeline (#12385) --- e2e/src/web/specs/shared-link.e2e-spec.ts | 11 +++++++++++ web/src/lib/utils.ts | 2 +- web/src/routes/+layout.svelte | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index fe7da0b2c0ead..8679bb3236b49 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -69,4 +69,15 @@ test.describe('Shared Links', () => { await page.goto('/share/invalid'); await page.getByRole('heading', { name: 'Invalid share key' }).waitFor(); }); + + test('auth on navigation from shared link to timeline', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto(`/share/${sharedLink.key}`); + await page.getByRole('heading', { name: 'Test Album' }).waitFor(); + + await page.locator('a[href="/"]').click(); + await page.waitForURL('/photos'); + await page.locator(`[data-asset-id="${asset.id}"]`).waitFor(); + }); }); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 6c3add70ce562..395d9796f4fb3 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -156,7 +156,7 @@ export const getJobName = derived(t, ($t) => { let _key: string | undefined; let _sharedLink: SharedLinkResponseDto | undefined; -export const setKey = (key: string) => (_key = key); +export const setKey = (key?: string) => (_key = key); export const getKey = (): string | undefined => _key; export const setSharedLink = (sharedLink: SharedLinkResponseDto) => (_sharedLink = sharedLink); export const getSharedLink = (): SharedLinkResponseDto | undefined => _sharedLink; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index b7335dea595d8..8f7f372efd266 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -71,6 +71,8 @@ } beforeNavigate(({ from, to }) => { + setKey(isSharedLinkRoute(to?.route.id) ? to?.params?.key : undefined); + if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) { return; } From 639bc0c66079bb917003017ccd935eef4976d625 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:16:18 +0200 Subject: [PATCH 07/26] fix(web): broken album thumbnail (#12381) * fix(web): broken album thumbnail * use properties from thumbnail --- .../asset-viewer/photo-viewer.svelte | 2 +- .../lib/components/assets/broken-asset.svelte | 33 +++++++++---------- .../assets/thumbnail/image-thumbnail.svelte | 21 +++++++----- .../covers/asset-cover.svelte | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 0a3da9ade3639..40a36fa0e09a5 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -152,7 +152,7 @@ ]} /> {#if imageError} - + {/if} diff --git a/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte index 216a8f6f848b0..dd54afba01ee9 100644 --- a/web/src/lib/components/assets/broken-asset.svelte +++ b/web/src/lib/components/assets/broken-asset.svelte @@ -3,23 +3,20 @@ import { mdiImageBrokenVariant } from '@mdi/js'; import { t } from 'svelte-i18n'; - export let square = false; - export let noMessage = false; + let className = ''; + export { className as class }; + export let hideMessage = false; + export let width: string | undefined = undefined; + export let height: string | undefined = undefined; -
-
- - {#if !noMessage} -
{$t('error_loading_image')}
- {/if} -
-
- -
-
-
+
+ + {#if !hideMessage} + {$t('error_loading_image')} + {/if} +
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 38f2ff4dbb506..662209544abbc 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -5,7 +5,6 @@ import { TUNABLES } from '$lib/utils/tunables'; import { mdiEyeOffOutline } from '@mdi/js'; import { onMount } from 'svelte'; - import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; export let url: string; @@ -45,12 +44,20 @@ setLoaded(); } }); + + $: optionalClasses = [ + curve && 'rounded-xl', + circle && 'rounded-full', + shadow && 'shadow-lg', + (circle || !heightStyle) && 'aspect-square', + border && 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary', + ] + .filter(Boolean) + .join(' '); {#if errored} - -
{$t('error_loading_image')}
-
+ {:else} {loaded diff --git a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte index 69c11e079c51b..d8b0a1b0d7bce 100644 --- a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte @@ -11,7 +11,7 @@ {#if isBroken} - + {:else} Date: Fri, 6 Sep 2024 15:16:39 +0200 Subject: [PATCH 08/26] fix(web): ensure shared link covers are full size (#12386) --- .../lib/components/album-page/__tests__/album-cover.spec.ts | 4 ++-- web/src/lib/components/album-page/album-card.svelte | 2 +- .../sharedlinks-page/covers/__tests__/asset-cover.spec.ts | 2 +- .../sharedlinks-page/covers/__tests__/no-cover.spec.ts | 2 +- .../sharedlinks-page/covers/__tests__/share-cover.spec.ts | 6 +++--- .../components/sharedlinks-page/covers/asset-cover.svelte | 2 +- .../lib/components/sharedlinks-page/covers/no-cover.svelte | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts index ec4878cd15044..5fa8b960087ec 100644 --- a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts @@ -19,7 +19,7 @@ describe('AlbumCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('someName'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square text'); expect(img.getAttribute('src')).toBe('/asdf'); expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' }); }); @@ -36,7 +36,7 @@ describe('AlbumCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('unnamed_album'); expect(img.getAttribute('loading')).toBe('eager'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square asdf'); expect(img.getAttribute('src')).toStrictEqual(expect.any(String)); }); }); diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index 60eda166ab934..f574c65f0b736 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -44,7 +44,7 @@
{/if} - +

{ expect(img.alt).toBe('123'); expect(img.getAttribute('src')).toBe('wee'); expect(img.getAttribute('loading')).toBe('eager'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square asdf'); }); }); diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts index bdf0b8878c406..bb87c02135d90 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts @@ -10,7 +10,7 @@ describe('NoCover component', () => { }); const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('123'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square asdf'); expect(img.getAttribute('loading')).toBe('eager'); expect(img.src).toStrictEqual(expect.any(String)); }); diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts index 83ca07b40e0e5..76de04ea3127a 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts @@ -17,7 +17,7 @@ describe('ShareCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('123'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square text'); }); it('renders an image when the shared link is an individual share', () => { @@ -30,7 +30,7 @@ describe('ShareCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('individual_share'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square text'); expect(img.getAttribute('src')).toBe('/asdf'); expect(getAssetThumbnailUrl).toHaveBeenCalledWith('someId'); }); @@ -44,7 +44,7 @@ describe('ShareCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('unnamed_share'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); + expect(img.className).toBe('size-full rounded-xl object-cover aspect-square text'); }); it.skip('renders fallback image when asset is not resized', () => { diff --git a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte index d8b0a1b0d7bce..bf5031e39d037 100644 --- a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte @@ -16,7 +16,7 @@ (isBroken = true)} - class="z-0 rounded-xl object-cover aspect-square {className}" + class="size-full rounded-xl object-cover aspect-square {className}" data-testid="album-image" draggable="false" loading={preload ? 'eager' : 'lazy'} diff --git a/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte index 45d7d4b315d21..087204d6a5df0 100644 --- a/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/no-cover.svelte @@ -7,7 +7,7 @@ Date: Fri, 6 Sep 2024 15:16:59 +0200 Subject: [PATCH 09/26] feat: optimize copy image to clipboard (#12366) * feat: optimize copy image to clipboard * pr feedback * fix: urlToBlob Co-authored-by: Jason Rasmussen * fix: imgToBlob Co-authored-by: Jason Rasmussen * chore: finish rename * fix: dimensions --------- Co-authored-by: Jason Rasmussen --- web/package-lock.json | 6 --- web/package.json | 1 - .../asset-viewer/asset-viewer-nav-bar.svelte | 4 +- .../asset-viewer/photo-viewer.svelte | 14 +++---- web/src/lib/utils/asset-utils.spec.ts | 8 +++- web/src/lib/utils/asset-utils.ts | 38 +++++++++++++++++++ 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 4ddc6d9baa966..7bcd5c2b01d47 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,7 +17,6 @@ "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", - "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", "handlebars": "^4.7.8", "intl-messageformat": "^10.5.14", @@ -3284,11 +3283,6 @@ "node": ">= 0.6" } }, - "node_modules/copy-image-clipboard": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz", - "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA==" - }, "node_modules/core-js-compat": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", diff --git a/web/package.json b/web/package.json index 1ba350022d98c..c84bbf0db43d3 100644 --- a/web/package.json +++ b/web/package.json @@ -73,7 +73,6 @@ "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", "@photo-sphere-viewer/video-plugin": "^5.7.2", "@zoom-image/svelte": "^0.2.6", - "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", "handlebars": "^4.7.8", "intl-messageformat": "^10.5.14", diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 0f75f9bb830f4..db216641d5c2f 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -41,7 +41,7 @@ mdiPresentationPlay, mdiUpload, } from '@mdi/js'; - import { canCopyImagesToClipboard } from 'copy-image-clipboard'; + import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { t } from 'svelte-i18n'; export let asset: AssetResponseDto; @@ -101,7 +101,7 @@ on:click={onZoomImage} /> {/if} - {#if canCopyImagesToClipboard() && asset.type === AssetTypeEnum.Image} + {#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image} {/if} diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 40a36fa0e09a5..4157c558d24cd 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -8,17 +8,17 @@ import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; - import { isWebCompatibleImage } from '$lib/utils/asset-utils'; + import { isWebCompatibleImage, canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; import { getAltText } from '$lib/utils/thumbnail-util'; import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; - import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard'; import { onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { type SwipeCustomEvent, swipe } from 'svelte-gestures'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; + import { handleError } from '$lib/utils/handle-error'; export let asset: AssetResponseDto; export let preloadAssets: AssetResponseDto[] | undefined = undefined; @@ -81,23 +81,19 @@ }; copyImage = async () => { - if (!canCopyImagesToClipboard()) { + if (!canCopyImageToClipboard()) { return; } try { - await copyImageToClipboard(assetFileUrl); + await copyImageToClipboard($photoViewer ?? assetFileUrl); notificationController.show({ type: NotificationType.Info, message: $t('copied_image_to_clipboard'), timeout: 3000, }); } catch (error) { - console.error('Error [photo-viewer]:', error); - notificationController.show({ - type: NotificationType.Error, - message: 'Copying image to clipboard failed.', - }); + handleError(error, $t('copy_error')); } }; diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts index 8970a6a65219c..b3a668192d57f 100644 --- a/web/src/lib/utils/asset-utils.spec.ts +++ b/web/src/lib/utils/asset-utils.spec.ts @@ -1,5 +1,5 @@ import type { AssetResponseDto } from '@immich/sdk'; -import { getAssetFilename, getFilenameExtension } from './asset-utils'; +import { canCopyImageToClipboard, getAssetFilename, getFilenameExtension } from './asset-utils'; describe('get file extension from filename', () => { it('returns the extension without including the dot', () => { @@ -56,3 +56,9 @@ describe('get asset filename', () => { } }); }); + +describe('copy image to clipboard', () => { + it('should not allow copy image to clipboard', () => { + expect(canCopyImageToClipboard()).toEqual(false); + }); +}); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index e309db5ff6a1e..84a896452f7f1 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -527,3 +527,41 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +export const canCopyImageToClipboard = (): boolean => { + return !!(navigator.clipboard && window.ClipboardItem); +}; + +const imgToBlob = async (imageElement: HTMLImageElement) => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + canvas.width = imageElement.naturalWidth; + canvas.height = imageElement.naturalHeight; + + if (context) { + context.drawImage(imageElement, 0, 0); + + return await new Promise((resolve) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + throw new Error('Canvas conversion to Blob failed'); + } + }); + }); + } + + throw new Error('Canvas context is null'); +}; + +const urlToBlob = async (imageSource: string) => { + const response = await fetch(imageSource); + return await response.blob(); +}; + +export const copyImageToClipboard = async (source: HTMLImageElement | string) => { + const blob = source instanceof HTMLImageElement ? await imgToBlob(source) : await urlToBlob(source); + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); +}; From 529b7fe748e5e5830b6b28b5130f220826ea0837 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:18:45 +0200 Subject: [PATCH 10/26] fix(web): show focus outline for asset thumbnails again (#12382) * fix(web): show focus outline for asset thumbnails again * fix e2e test --- e2e/src/web/specs/shared-link.e2e-spec.ts | 2 +- web/src/lib/components/assets/thumbnail/thumbnail.svelte | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index 8679bb3236b49..2a02e429a5900 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -44,7 +44,7 @@ test.describe('Shared Links', () => { test('download from a shared link', async ({ page }) => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); - await page.locator('.group').first().hover(); + await page.locator(`[data-asset-id="${asset.id}"]`).hover(); await page.waitForSelector('#asset-group-by-date svg'); await page.getByRole('checkbox').click(); await page.getByRole('button', { name: 'Download' }).click(); diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 69f777f530658..af22887185a36 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -175,7 +175,7 @@ data-int={intersecting} style:width="{width}px" style:height="{height}px" - class="group focus-visible:outline-none flex overflow-hidden {disabled + class="focus-visible:outline-none flex overflow-hidden {disabled ? 'bg-gray-300' : 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}" > @@ -193,6 +193,7 @@

Date: Fri, 6 Sep 2024 06:26:58 -0700 Subject: [PATCH 11/26] feat(web): add download shortcut on the timeline & asset viewer (#12339) feat(web): implement download shortcut --- .../lib/components/photos-page/actions/download-action.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte index 073d20901ca4e..7716fbe36d53d 100644 --- a/web/src/lib/components/photos-page/actions/download-action.svelte +++ b/web/src/lib/components/photos-page/actions/download-action.svelte @@ -2,6 +2,7 @@ import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { downloadArchive, downloadFile } from '$lib/utils/asset-utils'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; + import { shortcut } from '$lib/actions/shortcut'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { mdiCloudDownloadOutline, mdiFileDownloadOutline, mdiFolderDownloadOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -26,6 +27,8 @@ $: menuItemIcon = getAssets().size === 1 ? mdiFileDownloadOutline : mdiFolderDownloadOutline; + + {#if menuItem} {:else} From 5d8052202e4deb4b9f1f8f459b5ee0830ab20c00 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 08:30:26 -0500 Subject: [PATCH 12/26] chore(mobile): Translations update (#12392) chore(mobile): translation update --- mobile/assets/i18n/ar-JO.json | 4 ++ mobile/assets/i18n/cs-CZ.json | 6 ++- mobile/assets/i18n/da-DK.json | 4 ++ mobile/assets/i18n/de-DE.json | 4 ++ mobile/assets/i18n/el-GR.json | 4 ++ mobile/assets/i18n/en-US.json | 13 +++-- mobile/assets/i18n/es-ES.json | 34 ++++++------ mobile/assets/i18n/es-MX.json | 4 ++ mobile/assets/i18n/es-PE.json | 4 ++ mobile/assets/i18n/es-US.json | 4 ++ mobile/assets/i18n/fi-FI.json | 4 ++ mobile/assets/i18n/fr-CA.json | 4 ++ mobile/assets/i18n/fr-FR.json | 4 ++ mobile/assets/i18n/he-IL.json | 22 ++++---- mobile/assets/i18n/hi-IN.json | 4 ++ mobile/assets/i18n/hu-HU.json | 4 ++ mobile/assets/i18n/it-IT.json | 4 ++ mobile/assets/i18n/ja-JP.json | 4 ++ mobile/assets/i18n/ko-KR.json | 94 +++++++++++++++++---------------- mobile/assets/i18n/lt-LT.json | 4 ++ mobile/assets/i18n/lv-LV.json | 4 ++ mobile/assets/i18n/mn.json | 4 ++ mobile/assets/i18n/nb-NO.json | 4 ++ mobile/assets/i18n/nl-NL.json | 4 ++ mobile/assets/i18n/pl-PL.json | 4 ++ mobile/assets/i18n/pt-PT.json | 4 ++ mobile/assets/i18n/ro-RO.json | 4 ++ mobile/assets/i18n/ru-RU.json | 4 ++ mobile/assets/i18n/sk-SK.json | 4 ++ mobile/assets/i18n/sl-SI.json | 4 ++ mobile/assets/i18n/sr-Cyrl.json | 4 ++ mobile/assets/i18n/sr-Latn.json | 4 ++ mobile/assets/i18n/sv-FI.json | 4 ++ mobile/assets/i18n/sv-SE.json | 4 ++ mobile/assets/i18n/th-TH.json | 4 ++ mobile/assets/i18n/uk-UA.json | 4 ++ mobile/assets/i18n/vi-VN.json | 6 ++- mobile/assets/i18n/zh-CN.json | 4 ++ mobile/assets/i18n/zh-Hans.json | 4 ++ mobile/assets/i18n/zh-TW.json | 4 ++ 40 files changed, 235 insertions(+), 76 deletions(-) diff --git a/mobile/assets/i18n/ar-JO.json b/mobile/assets/i18n/ar-JO.json index 8b9f8c42c4041..fdc54da2b777f 100644 --- a/mobile/assets/i18n/ar-JO.json +++ b/mobile/assets/i18n/ar-JO.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "حذف الرابط المشترك", "description_input_hint_text": "اضف وصفا...", "description_input_submit_error": "خطأ تحديث الوصف ، تحقق من السجل لمزيد من التفاصيل", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "التاريخ و الوقت", "edit_date_time_dialog_timezone": "وحدة زمنية", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json index 9872aa13247ee..4a81de75960ab 100644 --- a/mobile/assets/i18n/cs-CZ.json +++ b/mobile/assets/i18n/cs-CZ.json @@ -63,7 +63,7 @@ "assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru", "asset_viewer_settings_title": "Prohlížeč", "backup_album_selection_page_albums_device": "Alba v zařízení ({})", - "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, dvojím klepnutím ji vyloučíte", + "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, opětovným klepnutím ji vyloučíte", "backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.", "backup_album_selection_page_select_albums": "Vybraná alba", "backup_album_selection_page_selection_info": "Informace o výběru", @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Odstranit sdílený odkaz", "description_input_hint_text": "Přidat popis...", "description_input_submit_error": "Chyba aktualizace popisu, další podrobnosti najdete v logu", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Datum a čas", "edit_date_time_dialog_timezone": "Časové pásmo", "edit_image_title": "Upravit", diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json index b9688a39c9f5d..20c3c43b09410 100644 --- a/mobile/assets/i18n/da-DK.json +++ b/mobile/assets/i18n/da-DK.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Slet delt link", "description_input_hint_text": "Tilføj en beskrivelse...", "description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Dato og klokkeslæt", "edit_date_time_dialog_timezone": "Tidszone", "edit_image_title": "Rediger", diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index 1da83fb551dae..bb2ed31f8a456 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Geteilten Link löschen", "description_input_hint_text": "Beschreibung hinzufügen...", "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Datum und Uhrzeit", "edit_date_time_dialog_timezone": "Zeitzone", "edit_image_title": "Bearbeiten", diff --git a/mobile/assets/i18n/el-GR.json b/mobile/assets/i18n/el-GR.json index 29efeb03d1353..88426a6076ba9 100644 --- a/mobile/assets/i18n/el-GR.json +++ b/mobile/assets/i18n/el-GR.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Διαγραφή Κοινοποιημένου Συνδέσμου", "description_input_hint_text": "Προσθήκη περιγραφής...", "description_input_submit_error": "Σφάλμα κατά την ενημέρωση της περιγραφής, ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Ημερομηνία και Ώρα", "edit_date_time_dialog_timezone": "Ζώνη ώρας", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 9dbe49589f75c..324c9069fdf46 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", @@ -252,10 +256,9 @@ "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_saved_successfully": "Image saved", - "download_error": "Download Error", - "download_started": "Download started", - "download_sucess": "Download success", - "download_sucess_android": "The media has been downloaded to DCIM/Immich", + "image_viewer_page_state_provider_download_error": "Download Error", + "image_viewer_page_state_provider_download_started": "Download Started", + "image_viewer_page_state_provider_download_success": "Download Success", "image_viewer_page_state_provider_share_error": "Share Error", "invalid_date": "Invalid date", "invalid_date_format": "Invalid date format", @@ -586,4 +589,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} +} \ No newline at end of file diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index 82e77aa4762c5..1943116b4ff5f 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -173,8 +173,8 @@ "control_bottom_app_bar_delete": "Eliminar", "control_bottom_app_bar_delete_from_immich": "Borrar de Immich", "control_bottom_app_bar_delete_from_local": "Borrar del dispositivo", - "control_bottom_app_bar_download": "Download", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_download": "Descargar", + "control_bottom_app_bar_edit": "Editar", "control_bottom_app_bar_edit_location": "Editar ubicación", "control_bottom_app_bar_edit_time": "Editar fecha y hora", "control_bottom_app_bar_favorite": "Favorito", @@ -190,7 +190,7 @@ "create_shared_album_page_share": "Compartir", "create_shared_album_page_share_add_assets": "AGREGAR ELEMENTOS", "create_shared_album_page_share_select_photos": "Seleccionar Fotos", - "crop": "Crop", + "crop": "Recortar", "curated_location_page_title": "Lugares", "curated_object_page_title": "Objetos", "daily_title_text_date": "E dd, MMM", @@ -210,9 +210,13 @@ "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Fecha y Hora", "edit_date_time_dialog_timezone": "Zona horaria", - "edit_image_title": "Edit", + "edit_image_title": "Editar", "edit_location_dialog_title": "Ubicación", "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Agregar Descripción...", @@ -251,13 +255,13 @@ "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", "home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo", "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", - "image_saved_successfully": "Image saved", + "image_saved_successfully": "Imágenes guardas", "image_viewer_page_state_provider_download_error": "Error de descarga", "image_viewer_page_state_provider_download_started": "Descarga Iniciada", "image_viewer_page_state_provider_download_success": "Descarga exitosa", "image_viewer_page_state_provider_share_error": "Error al compartir", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Fecha incorrecta", + "invalid_date_format": "Formato de fecha incorrecto", "library_page_albums": "Álbumes", "library_page_archive": "Archivo", "library_page_device_albums": "Álbumes en el dispositivo", @@ -380,27 +384,27 @@ "profile_drawer_sign_out": "Cerrar Sesión", "profile_drawer_trash": "Papelera", "recently_added_page_title": "Recién Agregadas", - "save_to_gallery": "Save to gallery", + "save_to_gallery": "Guardado en la galería", "scaffold_body_error_occurred": "Ha ocurrido un error", "search_bar_hint": "Busca tus fotos", "search_filter_apply": "Aplicar filtros", - "search_filter_camera": "Camera", + "search_filter_camera": "Cámara", "search_filter_camera_make": "Marca", "search_filter_camera_model": "Modelo", "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_date": "Fecha", + "search_filter_date_interval": "{start} al {end}", + "search_filter_date_title": "Selecciona un intervalo de fechas", "search_filter_display_option_archive": "Archivado", "search_filter_display_option_favorite": "Favorito", "search_filter_display_option_not_in_album": "No en álbum", "search_filter_display_options": "Display Options", "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_location": "Ubicación", "search_filter_location_city": "Ciudad", "search_filter_location_country": "País", "search_filter_location_state": "Estado", - "search_filter_location_title": "Select location", + "search_filter_location_title": "Seleccionar una ubicación", "search_filter_media_type": "Media Type", "search_filter_media_type_all": "Todos", "search_filter_media_type_image": "Imagen", @@ -535,7 +539,7 @@ "sharing_silver_appbar_create_shared_album": "Crear un álbum compartido", "sharing_silver_appbar_shared_links": "Enlaces compartidos", "sharing_silver_appbar_share_partner": "Compartir con el compañero", - "sync": "Sync", + "sync": "Sincronizar", "sync_albums": "Sync albums", "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json index 80eeae8d39df4..8361e9a285107 100644 --- a/mobile/assets/i18n/es-MX.json +++ b/mobile/assets/i18n/es-MX.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json index 4971435f9ee3b..cee06c9512cd2 100644 --- a/mobile/assets/i18n/es-PE.json +++ b/mobile/assets/i18n/es-PE.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json index cff40b28ba009..ea0b328a808d4 100644 --- a/mobile/assets/i18n/es-US.json +++ b/mobile/assets/i18n/es-US.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json index 410a7e4719f40..cb687ecef5f24 100644 --- a/mobile/assets/i18n/fi-FI.json +++ b/mobile/assets/i18n/fi-FI.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Poista jaettu linkki", "description_input_hint_text": "Lisää kuvaus...", "description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Päivämäärä ja aika", "edit_date_time_dialog_timezone": "Aikavyöhyke", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json index 97a23c4cc70d1..8d742c3a5943e 100644 --- a/mobile/assets/i18n/fr-CA.json +++ b/mobile/assets/i18n/fr-CA.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Supprimer le lien partagé", "description_input_hint_text": "Ajouter une description...", "description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index fe280aa4d27f6..9ff5c6f28031e 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Supprimer le lien partagé", "description_input_hint_text": "Ajouter une description…", "description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date et heure", "edit_date_time_dialog_timezone": "Fuseau horaire", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/he-IL.json b/mobile/assets/i18n/he-IL.json index b57b6c01d6b93..7ddbb392a0cf6 100644 --- a/mobile/assets/i18n/he-IL.json +++ b/mobile/assets/i18n/he-IL.json @@ -190,7 +190,7 @@ "create_shared_album_page_share": "שתף", "create_shared_album_page_share_add_assets": "הוסף נכסים", "create_shared_album_page_share_select_photos": "בחירת תמונות", - "crop": "Crop", + "crop": "חתוך", "curated_location_page_title": "מקומות", "curated_object_page_title": "דברים", "daily_title_text_date": "E, MMM dd", @@ -210,11 +210,15 @@ "delete_shared_link_dialog_title": "מחק קישור משותף", "description_input_hint_text": "הוסף תיאור...", "description_input_submit_error": "שגיאה בעדכון תיאור, בדוק את היומן לפרטים נוספים", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "תאריך וזמן", "edit_date_time_dialog_timezone": "אזור זמן", - "edit_image_title": "Edit", + "edit_image_title": "ערוך", "edit_location_dialog_title": "מיקום", - "error_saving_image": "Error: {}", + "error_saving_image": "שגיאה: {}", "exif_bottom_sheet_description": "הוסף תיאור...", "exif_bottom_sheet_details": "פרטים", "exif_bottom_sheet_location": "מיקום", @@ -251,7 +255,7 @@ "home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא להקפיד לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים)", "home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג", "home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 נכסים בכל פעם, מדלג", - "image_saved_successfully": "Image saved", + "image_saved_successfully": "תמונה נשמרה", "image_viewer_page_state_provider_download_error": "שגיאת הורדה", "image_viewer_page_state_provider_download_started": "ההורדה החלה", "image_viewer_page_state_provider_download_success": "הצלחת הורדה", @@ -380,7 +384,7 @@ "profile_drawer_sign_out": "יציאה", "profile_drawer_trash": "אשפה", "recently_added_page_title": "נוסף לאחרונה", - "save_to_gallery": "Save to gallery", + "save_to_gallery": "שמור לגלריה", "scaffold_body_error_occurred": "אירעה שגיאה", "search_bar_hint": "חפש/י בתמונות שלך", "search_filter_apply": "החל סינון", @@ -535,10 +539,10 @@ "sharing_silver_appbar_create_shared_album": "אלבום משותף חדש", "sharing_silver_appbar_shared_links": "קישורים משותפים", "sharing_silver_appbar_share_partner": "שיתוף עם שותף", - "sync": "Sync", - "sync_albums": "Sync albums", - "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync": "סנכרן", + "sync_albums": "סנכרן אלבומים", + "sync_albums_manual_subtitle": "סנכרן את כל הסרטונים והתמונות שהועלו לאלבומי הגיבוי שנבחרו", + "sync_upload_album_setting_subtitle": "צור והעלה תמונות וסרטונים שלך לאלבומים שנבחרו ביישום", "tab_controller_nav_library": "ספרייה", "tab_controller_nav_photos": "תמונות", "tab_controller_nav_search": "חיפוש", diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json index 7d415cc2f8607..534cae0622d9d 100644 --- a/mobile/assets/i18n/hi-IN.json +++ b/mobile/assets/i18n/hi-IN.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "साझा किए गए लिंक को हटाएं", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json index 72012b1ca3cfd..8f14b9673a578 100644 --- a/mobile/assets/i18n/hu-HU.json +++ b/mobile/assets/i18n/hu-HU.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Megosztott Link Törlése", "description_input_hint_text": "Leírás hozzáadása...", "description_input_submit_error": "Nem sikerült frissíteni a leírást. További információért kérjük, nézd meg az eseménynaplót", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Dátum és Idő", "edit_date_time_dialog_timezone": "Időzóna", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json index febac12c05927..d7585c753c99b 100644 --- a/mobile/assets/i18n/it-IT.json +++ b/mobile/assets/i18n/it-IT.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Elimina link condiviso", "description_input_hint_text": "Aggiungi descrizione...", "description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Data e ora", "edit_date_time_dialog_timezone": "Fuso orario", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/ja-JP.json b/mobile/assets/i18n/ja-JP.json index ccd78380a732b..21b8bea9e35e9 100644 --- a/mobile/assets/i18n/ja-JP.json +++ b/mobile/assets/i18n/ja-JP.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "共有リンクを消す", "description_input_hint_text": "説明を追加", "description_input_submit_error": "説明の編集に失敗しました。詳細はログを確認してください。", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "日付と時間", "edit_date_time_dialog_timezone": "タイムゾーン", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index bf381fee22dac..e6da75c2f6f28 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -31,7 +31,7 @@ "album_viewer_appbar_share_err_delete": "앨범을 삭제하지 못했습니다.", "album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다.", "album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하지 못했습니다.", - "album_viewer_appbar_share_err_title": "앨범 이름을 변경하지 못했습니다.", + "album_viewer_appbar_share_err_title": "앨범명을 변경하지 못했습니다.", "album_viewer_appbar_share_leave": "앨범 나가기", "album_viewer_appbar_share_remove": "앨범에서 제거", "album_viewer_appbar_share_to": "공유 대상", @@ -55,12 +55,12 @@ "asset_list_settings_subtitle": "사진 배열 레이아웃 설정", "asset_list_settings_title": "사진 배열", "asset_restored_successfully": "항목이 성공적으로 복원되었습니다.", - "assets_deleted_permanently": "{} 미디어가 영구 삭제됨", - "assets_deleted_permanently_from_server": "Immich 서버에서 {} 미디어가 영구 삭제되었습니다.", - "assets_removed_permanently_from_device": "장치에서 {} 미디어가 영구적으로 제거되었습니다.", - "assets_restored_successfully": "{} 미디어가 성공적으로 복원되었습니다.", - "assets_trashed": "{} 미디어가 휴지통에 버려졌습니다.", - "assets_trashed_from_server": "Immich 서버에서 {} 미디어를 삭제했습니다.", + "assets_deleted_permanently": "{}개 항목이 영구적으로 삭제됨", + "assets_deleted_permanently_from_server": "{}개 항목이 Immich 서버에서 영구적으로 삭제됨", + "assets_removed_permanently_from_device": "{}개 항목이 기기에서 영구적으로 삭제됨", + "assets_restored_successfully": "항목 {}개를 복원했습니다.", + "assets_trashed": "휴지통으로 {}개 항목이 이동되었습니다.", + "assets_trashed_from_server": "휴지통으로 Immich 서버의 {}개 항목이 이동되었습니다.", "asset_viewer_settings_title": "보기 옵션", "backup_album_selection_page_albums_device": "기기의 앨범 ({})", "backup_album_selection_page_albums_tap": "한 번 눌러 선택, 두 번 눌러 제외하세요.", @@ -69,55 +69,55 @@ "backup_album_selection_page_selection_info": "선택한 앨범", "backup_album_selection_page_total_assets": "전체 항목", "backup_all": "모두", - "backup_background_service_backup_failed_message": "백업하지 못했습니다. 다시 시도하는 중...", + "backup_background_service_backup_failed_message": "항목을 백업하지 못했습니다. 다시 시도하는 중...", "backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...", "backup_background_service_current_upload_notification": "{} 업로드 중", "backup_background_service_default_notification": "백업할 항목을 확인하는 중...", "backup_background_service_error_title": "백업 오류", "backup_background_service_in_progress_notification": "선택한 항목을 백업하는 중...", "backup_background_service_upload_failure_notification": "{} 업로드 실패", - "backup_controller_page_albums": "백업 대상 앨범", + "backup_controller_page_albums": "백업할 앨범", "backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화하세요.", "backup_controller_page_background_app_refresh_disabled_title": "백그라운드 새로 고침 비활성화됨", "backup_controller_page_background_app_refresh_enable_button_text": "설정으로 이동", "backup_controller_page_background_battery_info_link": "설정 방법", - "backup_controller_page_background_battery_info_message": "최상의 백그라운드 백업 환경을 위해, Immich의 백그라운드 활동을 제한하는 배터리 최적화를 비활성화하세요.\n\n설정 방법은 기기마다 다르므로, 제조 업체에서 관련 정보를 찾아보세요.", + "backup_controller_page_background_battery_info_message": "최상의 백그라운드 백업 환경을 위해 Immich 백그라운드 활동을 제한하는 배터리 최적화 기능을 비활성화하세요.\n\n기기마다 설정 방법에 차이가 있어 제조 업체에서 관련 정보를 찾아보세요.", "backup_controller_page_background_battery_info_ok": "확인", "backup_controller_page_background_battery_info_title": "배터리 최적화", "backup_controller_page_background_charging": "충전 중에만", "backup_controller_page_background_configure_error": "백그라운드 서비스 구성 실패", "backup_controller_page_background_delay": "새 콘텐츠 백업 간격: {}", - "backup_controller_page_background_description": "백그라운드 서비스를 활성화하면 앱을 열지 않고도 새 콘텐츠를 자동으로 백업할 수 있습니다.", - "backup_controller_page_background_is_off": "자동 백그라운드 백업이 비활성화되었습니다.", - "backup_controller_page_background_is_on": "자동 백그라운드 백업이 활성화되었습니다.", + "backup_controller_page_background_description": "백그라운드 서비스를 활성화하여 앱을 실행하지 않고 새 항목을 자동으로 백업하세요.", + "backup_controller_page_background_is_off": "백그라운드 백업이 비활성화되었습니다.", + "backup_controller_page_background_is_on": "백그라운드 백업이 활성화되었습니다.", "backup_controller_page_background_turn_off": "백그라운드 서비스 비활성화", "backup_controller_page_background_turn_on": "백그라운드 서비스 활성화", "backup_controller_page_background_wifi": "Wi-Fi에서만", "backup_controller_page_backup": "백업", - "backup_controller_page_backup_selected": "선택: ", + "backup_controller_page_backup_selected": "선택됨:", "backup_controller_page_backup_sub": "백업된 사진 및 동영상", "backup_controller_page_cancel": "취소", "backup_controller_page_created": "생성일: {}", - "backup_controller_page_desc_backup": "앱을 열 때 새 항목을 서버에 자동으로 업로드하려면 포그라운드 백업을 활성화하세요.", - "backup_controller_page_excluded": "제외: ", + "backup_controller_page_desc_backup": "포그라운드 백업을 활성화하여 앱을 시작할 때 새 항목을 서버에 자동으로 업로드하세요.", + "backup_controller_page_excluded": "제외됨:", "backup_controller_page_failed": "실패 ({})", "backup_controller_page_filename": "파일명: {} [{}]", "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "백업 정보", "backup_controller_page_none_selected": "선택한 항목이 없습니다.", "backup_controller_page_remainder": "남은 항목", - "backup_controller_page_remainder_sub": "백업할 사진 및 동영상", + "backup_controller_page_remainder_sub": "백업 대기 중인 사진 및 동영상", "backup_controller_page_select": "선택", "backup_controller_page_server_storage": "저장 공간", "backup_controller_page_start_backup": "백업 시작", - "backup_controller_page_status_off": "자동 백업이 비활성화되었습니다.", - "backup_controller_page_status_on": "자동 백업이 활성화되었습니다.", + "backup_controller_page_status_off": "포그라운드 백업이 비활성화되었습니다.", + "backup_controller_page_status_on": "포그라운드 백업이 활성화되었습니다.", "backup_controller_page_storage_format": "{} 사용 중, 전체 {}", - "backup_controller_page_to_backup": "백업 대상 앨범 목록", + "backup_controller_page_to_backup": "백업할 앨범 목록", "backup_controller_page_total": "전체", - "backup_controller_page_total_sub": "선택한 앨범의 모든 사진 및 동영상", - "backup_controller_page_turn_off": "백업 비활성화", - "backup_controller_page_turn_on": "백업 활성화", + "backup_controller_page_total_sub": "선택한 앨범의 고유한 사진 및 동영상", + "backup_controller_page_turn_off": "비활성화", + "backup_controller_page_turn_on": "활성화", "backup_controller_page_uploading_file_info": "파일 정보 업로드 중", "backup_err_only_album": "유일한 앨범은 제거할 수 없습니다.", "backup_info_card_assets": "항목", @@ -154,7 +154,7 @@ "client_cert_enter_password": "비밀번호 입력", "client_cert_import": "가져오기", "client_cert_import_success_msg": "클라이언트 인증서를 가져왔습니다.", - "client_cert_invalid_msg": "유효하지 않은 인증서이거나 비밀번호가 일치하지 않습니다.", + "client_cert_invalid_msg": "유효하지 않은 인증서 또는 패스프레이즈가 일치하지 않습니다.", "client_cert_remove": "제거", "client_cert_remove_msg": "클라이언트 인증서가 제거되었습니다.", "client_cert_subtitle": "인증서 가져오기/제거는 로그인 전에만 가능합니다. PKCS12 (.p12, .pfx) 형식을 지원합니다.", @@ -210,11 +210,15 @@ "delete_shared_link_dialog_title": "공유 링크 삭제", "description_input_hint_text": "설명 추가...", "description_input_submit_error": "설명을 변경하는 중 문제가 발생했습니다. 자세한 내용은 로그를 참조하세요.", + "download_error": "다운로드 중 문제가 발생했습니다.", + "download_started": "다운로드가 시작되었습니다.", + "download_sucess": "다운로드가 완료되었습니다.", + "download_sucess_android": "미디어가 DCIM/Immich에 저장되었습니다.", "edit_date_time_dialog_date_time": "날짜 및 시간", "edit_date_time_dialog_timezone": "시간대", "edit_image_title": "편집", "edit_location_dialog_title": "위치", - "error_saving_image": "오류입니다: {}", + "error_saving_image": "오류: {}", "exif_bottom_sheet_description": "설명 추가...", "exif_bottom_sheet_details": "상세 정보", "exif_bottom_sheet_location": "위치", @@ -251,7 +255,7 @@ "home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인에 앨범의 사진과 동영상을 채울 수 있도록 백업할 앨범을 선택하세요.", "home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.", "home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.", - "image_saved_successfully": "이미지 저장", + "image_saved_successfully": "이미지가 저장되었습니다.", "image_viewer_page_state_provider_download_error": "다운로드 오류", "image_viewer_page_state_provider_download_started": "다운로드가 시작되었습니다.", "image_viewer_page_state_provider_download_success": "다운로드 완료", @@ -282,14 +286,14 @@ "login_form_back_button_text": "뒤로", "login_form_button_text": "로그인", "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "https://your-server-ip:port/api", + "login_form_endpoint_hint": "http://your-server-ip:port/api", "login_form_endpoint_url": "서버 엔드포인트 URL", "login_form_err_http": "http:// 또는 https://로 시작해야 합니다.", "login_form_err_invalid_email": "유효하지 않은 이메일", "login_form_err_invalid_url": "잘못된 URL입니다.", - "login_form_err_leading_whitespace": "앞에 공백 문자가 있습니다.", - "login_form_err_trailing_whitespace": "뒤에 공백 문자가 있습니다.", - "login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인해주세요.", + "login_form_err_leading_whitespace": "시작 부분에 공백이 있습니다.", + "login_form_err_trailing_whitespace": "끝 부분에 공백이 있습니다.", + "login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인하세요.", "login_form_failed_get_oauth_server_disable": "이 서버는 OAuth 기능을 지원하지 않습니다.", "login_form_failed_login": "로그인 오류. 서버 URL, 이메일 및 비밀번호를 확인하세요.", "login_form_handshake_exception": "서버와 통신 중 인증서 예외가 발생했습니다. 자체 서명된 인증서를 사용 중이라면, 설정에서 자체 서명된 인증서 허용을 활성화하세요.", @@ -343,7 +347,7 @@ "notification_permission_dialog_cancel": "취소", "notification_permission_dialog_content": "알림을 활성화하려면 설정에서 알림 권한을 허용하세요.", "notification_permission_dialog_settings": "설정", - "notification_permission_list_tile_content": "알림을 활성화하기 위해 권한을 부여하세요.", + "notification_permission_list_tile_content": "알림을 활성화하려면 권한을 부여하세요.", "notification_permission_list_tile_enable_button": "알림 활성화", "notification_permission_list_tile_title": "알림 권한", "partner_list_user_photos": "{user}님의 사진", @@ -371,7 +375,7 @@ "profile_drawer_app_logs": "로그", "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", - "profile_drawer_client_server_up_to_date": "모바일 앱과 서버가 최신 버전입니다.", + "profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신입니다.", "profile_drawer_documentation": "문서", "profile_drawer_github": "Github", "profile_drawer_server_out_of_date_major": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", @@ -389,7 +393,7 @@ "search_filter_camera_model": "모델명", "search_filter_camera_title": "카메라 종류 선택", "search_filter_date": "날짜", - "search_filter_date_interval": "{start}에서 {end} 까지", + "search_filter_date_interval": "{start} - {end}", "search_filter_date_title": "날짜 범위 선택", "search_filter_display_option_archive": "보관함", "search_filter_display_option_favorite": "즐겨찾기", @@ -411,8 +415,8 @@ "search_page_categories": "분류", "search_page_favorites": "즐겨찾기", "search_page_motion_photos": "모션 포토", - "search_page_no_objects": "사물 정보가 없습니다.", - "search_page_no_places": "장소 정보가 없습니다.", + "search_page_no_objects": "사용 가능한 사물 정보 없음", + "search_page_no_places": "사용 가능한 위치 정보 없음", "search_page_people": "인물", "search_page_person_add_name_dialog_cancel": "취소", "search_page_person_add_name_dialog_hint": "이름", @@ -435,7 +439,7 @@ "search_suggestion_list_smart_search_hint_2": "m:your-search-term", "select_additional_user_for_sharing_page_suggestions": "추천", "select_user_for_sharing_page_err_album": "앨범을 생성하지 못했습니다.", - "select_user_for_sharing_page_share_suggestions": "추천", + "select_user_for_sharing_page_share_suggestions": "제안", "server_info_box_app_version": "앱 버전", "server_info_box_latest_release": "최신 버전", "server_info_box_server_url": "서버 URL", @@ -454,12 +458,12 @@ "setting_notifications_notify_minutes": "{}분 후", "setting_notifications_notify_never": "알리지 않음", "setting_notifications_notify_seconds": "{}초", - "setting_notifications_single_progress_subtitle": "각 항목의 세부 업로드 정보 표시", - "setting_notifications_single_progress_title": "백그라운드 작업의 세부 진행률 표시", + "setting_notifications_single_progress_subtitle": "개별 항목의 상세 업로드 정보 표시", + "setting_notifications_single_progress_title": "백그라운드 백업 상세 진행률 표시", "setting_notifications_subtitle": "알림 기본 설정 조정", "setting_notifications_title": "알림", "setting_notifications_total_progress_subtitle": "전체 업로드 진행률 (완료/전체)", - "setting_notifications_total_progress_title": "백그라운드 작업의 전체 진행률 표시", + "setting_notifications_total_progress_title": "백그라운드 백업 전체 진행률 표시", "setting_pages_app_bar_settings": "설정", "settings_require_restart": "설정을 적용하려면 Immich를 다시 시작하세요.", "setting_video_viewer_looping_subtitle": "상세 보기에서 동영상을 자동으로 반복합니다.", @@ -467,7 +471,7 @@ "setting_video_viewer_title": "동영상", "share_add": "추가", "share_add_photos": "사진 추가", - "share_add_title": "앨범 제목 입력", + "share_add_title": "앨범명 추가", "share_assets_selected": "{}개 항목 선택됨", "share_create_album": "앨범 생성", "shared_album_activities_input_disable": "댓글이 비활성화되었습니다", @@ -528,7 +532,7 @@ "shared_link_manage_links": "공유 링크 관리", "shared_link_public_album": "공개 앨범", "share_done": "완료", - "share_invite": "앨범에 초대", + "share_invite": "앨범으로 초대", "sharing_page_album": "공유 앨범", "sharing_page_description": "공유 앨범을 만들어 주변 사람들과 사진 및 동영상을 공유하세요.", "sharing_page_empty_list": "공유 앨범 없음", @@ -537,8 +541,8 @@ "sharing_silver_appbar_share_partner": "파트너와 공유", "sync": "동기화", "sync_albums": "앨범 동기화", - "sync_albums_manual_subtitle": "업로드한 모든 동영상과 사진을 선택한 백업 앨범에 동기화합니다.", - "sync_upload_album_setting_subtitle": "Immich에서 선택한 앨범에 사진 및 동영상을 만들고 업로드하세요.", + "sync_albums_manual_subtitle": "업로드한 모든 동영상과 사진을 선택한 백업 앨범에 동기화", + "sync_upload_album_setting_subtitle": "선택한 앨범을 Immich에 생성하고 사진 및 동영상을 업로드하세요.", "tab_controller_nav_library": "라이브러리", "tab_controller_nav_photos": "사진", "tab_controller_nav_search": "검색", @@ -546,7 +550,7 @@ "theme_setting_asset_list_storage_indicator_title": "항목에 스토리지 동기화 여부 표시", "theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({})", "theme_setting_colorful_interface_subtitle": "배경에 대표 색상을 적용합니다.", - "theme_setting_colorful_interface_title": "컬러풀 인터페이스", + "theme_setting_colorful_interface_title": "미려한 인터페이스", "theme_setting_dark_mode_switch": "다크 모드", "theme_setting_image_viewer_quality_subtitle": "상세 보기 이미지 품질 조정", "theme_setting_image_viewer_quality_title": "이미지 보기 품질", @@ -559,7 +563,7 @@ "theme_setting_three_stage_loading_subtitle": "이 기능은 앱의 로드 성능을 향상시킬 수 있지만 더 많은 데이터를 사용합니다.", "theme_setting_three_stage_loading_title": "3단계 로드 활성화", "translated_text_options": "옵션", - "trash_emptied": "휴지통 비우기", + "trash_emptied": "휴지통을 비움", "trash_page_delete": "삭제", "trash_page_delete_all": "모두 삭제", "trash_page_empty_trash_btn": "휴지통 비우기", diff --git a/mobile/assets/i18n/lt-LT.json b/mobile/assets/i18n/lt-LT.json index 3d1fb4e4d6f3e..324c9069fdf46 100644 --- a/mobile/assets/i18n/lt-LT.json +++ b/mobile/assets/i18n/lt-LT.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json index 70af1ac37eae5..c9f86535fc5a3 100644 --- a/mobile/assets/i18n/lv-LV.json +++ b/mobile/assets/i18n/lv-LV.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Dzēst Kopīgošanas saiti", "description_input_hint_text": "Pievienot aprakstu...", "description_input_submit_error": "Atjauninot aprakstu, radās kļūda; papildinformāciju skatiet žurnālā", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Datums un Laiks", "edit_date_time_dialog_timezone": "Laika zona", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn.json index 5ebabbee8c0c2..cf951cea0b50b 100644 --- a/mobile/assets/i18n/mn.json +++ b/mobile/assets/i18n/mn.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index a5f151db597bb..7141faef72687 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Slett delt link", "description_input_hint_text": "Legg til beskrivelse ...", "description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Dato og tid", "edit_date_time_dialog_timezone": "Tidssone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index f34480f9858a1..a6a151d5069f0 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Verwijder gedeelde link", "description_input_hint_text": "Beschrijving toevoegen...", "description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details", + "download_error": "Fout bij downloaden", + "download_started": "Download gestart", + "download_sucess": "Succesvol gedownload", + "download_sucess_android": "Het bestand is gedownload naar DCIM/Immich", "edit_date_time_dialog_date_time": "Datum en tijd", "edit_date_time_dialog_timezone": "Tijdzone", "edit_image_title": "Bewerken", diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index 8a737d31d5312..ec9009e28ff65 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Usuń udostępniony link", "description_input_hint_text": "Dodaj opis...", "description_input_submit_error": "Błąd aktualizacji opisu, sprawdź dziennik, aby uzyskać więcej szczegółów", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Data i godzina", "edit_date_time_dialog_timezone": "Strefa czasowa", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/pt-PT.json b/mobile/assets/i18n/pt-PT.json index 769289b59c903..991fdfaf361f4 100644 --- a/mobile/assets/i18n/pt-PT.json +++ b/mobile/assets/i18n/pt-PT.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Excluir link compartilhado", "description_input_hint_text": "Adicionar descrição...", "description_input_submit_error": "Erro ao atualizar a descrição, verifique o registo para obter mais detalhes", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Data e Hora", "edit_date_time_dialog_timezone": "Fuso horário", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/ro-RO.json b/mobile/assets/i18n/ro-RO.json index 8ac8601714a1e..4cb043d1962d2 100644 --- a/mobile/assets/i18n/ro-RO.json +++ b/mobile/assets/i18n/ro-RO.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Șterge link distribuire", "description_input_hint_text": "Adaugă descriere...", "description_input_submit_error": "Eroare actualizare descriere, verifică log-urile pentru mai multe detalii", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Dată și Oră", "edit_date_time_dialog_timezone": "Fus orar", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index 8679b46df39df..1c5741a963ef2 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Удалить общую ссылку", "description_input_hint_text": "Добавить описание...", "description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Дата и время", "edit_date_time_dialog_timezone": "Часовой пояс", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json index 2c48a7f6c5e8a..200db9e32038b 100644 --- a/mobile/assets/i18n/sk-SK.json +++ b/mobile/assets/i18n/sk-SK.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Odstrániť zdieľaný odkaz", "description_input_hint_text": "Pridať popis...", "description_input_submit_error": "Chyba pri aktualizovaní popisu, zobrazte log pre viac detailov", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Dátum a čas", "edit_date_time_dialog_timezone": "Časové pásmo", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/sl-SI.json b/mobile/assets/i18n/sl-SI.json index c7018f7b6fa53..7871d65de9b90 100644 --- a/mobile/assets/i18n/sl-SI.json +++ b/mobile/assets/i18n/sl-SI.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Izbriši povezavo skupne rabe", "description_input_hint_text": "Dodaj opis ...", "description_input_submit_error": "Napaka pri posodabljanju opisa, preverite dnevnik za več podrobnosti", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Datum in ura", "edit_date_time_dialog_timezone": "Časovni pas", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json index 3d1fb4e4d6f3e..324c9069fdf46 100644 --- a/mobile/assets/i18n/sr-Cyrl.json +++ b/mobile/assets/i18n/sr-Cyrl.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json index 31a0d0f48e352..744ebe72ce63f 100644 --- a/mobile/assets/i18n/sr-Latn.json +++ b/mobile/assets/i18n/sr-Latn.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json index 3d1fb4e4d6f3e..324c9069fdf46 100644 --- a/mobile/assets/i18n/sv-FI.json +++ b/mobile/assets/i18n/sv-FI.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index d51bdd54ed146..0d6c7a310855d 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Ta Bort Delad Länk", "description_input_hint_text": "Lägg till beskrivning...", "description_input_submit_error": "Fel vid uppdatering av beskrivning, se loggen för fler detaljer", + "download_error": "Fel vid nedladdning", + "download_started": "Nedladdning påbörjad", + "download_sucess": "Nedladdning lyckades", + "download_sucess_android": "Media har laddats ner till DCIM/Immich", "edit_date_time_dialog_date_time": "Datum och Tid", "edit_date_time_dialog_timezone": "Tidszon", "edit_image_title": "Redigera", diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json index 9a45ff463a280..c93b0a37cfa12 100644 --- a/mobile/assets/i18n/th-TH.json +++ b/mobile/assets/i18n/th-TH.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "ลบลิงก์ที่แชร์", "description_input_hint_text": "เพื่มรายละเอียด...", "description_input_submit_error": "อัพเดตรายละเอียดผิดพลาด ตรวจสอบ log เพื่อรายละเอียดเพิ่มเติม", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "วันและเวลา", "edit_date_time_dialog_timezone": "เขดเวลา", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index 816ef5277f9d9..f3b2b0ba5f4b4 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Видалити спільне посилання", "description_input_hint_text": "Додати опис...", "description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Дата і час", "edit_date_time_dialog_timezone": "Часовий пояс", "edit_image_title": "Edit", diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index 31eff88dd7eaf..6cd2a080e4713 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -94,7 +94,7 @@ "backup_controller_page_background_turn_on": "Bật dịch vụ nền", "backup_controller_page_background_wifi": "Chỉ khi dùng Wi-Fi", "backup_controller_page_backup": "Sao lưu", - "backup_controller_page_backup_selected": "Đã chọn:", + "backup_controller_page_backup_selected": "Đã chọn: ", "backup_controller_page_backup_sub": "Ảnh và video đã sao lưu", "backup_controller_page_cancel": "Từ chối", "backup_controller_page_created": "Tạo vào: {}", @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Xoá liên kết đã chia sẻ", "description_input_hint_text": "Thêm mô tả...", "description_input_submit_error": "Cập nhật mô tả không thành công, vui lòng kiểm tra nhật ký để biết thêm chi tiết", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Ngày và Giờ", "edit_date_time_dialog_timezone": "Múi giờ", "edit_image_title": "Sửa", diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json index e421fff575f53..d4e7f0406e3aa 100644 --- a/mobile/assets/i18n/zh-CN.json +++ b/mobile/assets/i18n/zh-CN.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "删除共享链接", "description_input_hint_text": "添加描述...", "description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息", + "download_error": "下载出错", + "download_started": "开始下载", + "download_sucess": "下载成功", + "download_sucess_android": "媒体已下载至 DCIM/Immich", "edit_date_time_dialog_date_time": "日期和时间", "edit_date_time_dialog_timezone": "时区", "edit_image_title": "编辑", diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json index d98299a046634..f5ec6ab2a1bc8 100644 --- a/mobile/assets/i18n/zh-Hans.json +++ b/mobile/assets/i18n/zh-Hans.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "删除共享链接", "description_input_hint_text": "添加描述...", "description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息", + "download_error": "下载出错", + "download_started": "开始下载", + "download_sucess": "下载成功", + "download_sucess_android": "媒体已下载至 DCIM/Immich", "edit_date_time_dialog_date_time": "日期和时间", "edit_date_time_dialog_timezone": "时区", "edit_image_title": "编辑", diff --git a/mobile/assets/i18n/zh-TW.json b/mobile/assets/i18n/zh-TW.json index 3d1fb4e4d6f3e..324c9069fdf46 100644 --- a/mobile/assets/i18n/zh-TW.json +++ b/mobile/assets/i18n/zh-TW.json @@ -210,6 +210,10 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", + "download_error": "Download Error", + "download_started": "Download started", + "download_sucess": "Download success", + "download_sucess_android": "The media has been downloaded to DCIM/Immich", "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_image_title": "Edit", From 068904f7461118d4e4d6f8733e7e4186b6c38c84 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:49:08 +0000 Subject: [PATCH 13/26] chore: version v1.114.0 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 3778fac8c0f13..f443c141b9e06 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.17", + "version": "2.2.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.17", + "version": "2.2.18", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.113.1", + "version": "1.114.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index efb52c8afac4e..0d560c8456585 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.17", + "version": "2.2.18", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index c6c7832b62d46..c16413f4c5f49 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.114.0", + "url": "https://v1.114.0.archive.immich.app" + }, { "label": "v1.113.1", "url": "https://v1.113.1.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 7c54a3f227bc2..97e396c09f1b0 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.113.1", + "version": "1.114.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.113.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.17", + "version": "2.2.18", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.113.1", + "version": "1.114.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 3da113da35247..3577bc4510a9e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.113.1", + "version": "1.114.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index ac2ff0e34e72b..a69fb33a8d50e 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.113.1" +version = "1.114.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 0338c6ff343af..c127032b19ee2 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 157, - "android.injected.version.name" => "1.113.1", + "android.injected.version.code" => 158, + "android.injected.version.name" => "1.114.0", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 68b577c9c9bdb..c1740771d98c4 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.113.1" + version_number: "1.114.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b67a3e33839e1..bb845157979b6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.113.1 +- API version: 1.114.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 728b90c3f330a..3db5457c8c1c1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.113.1+157 +version: 1.114.0+158 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index bbfabfe1d7bf6..2325f24ee59d4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7394,7 +7394,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.113.1", + "version": "1.114.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 39140326519d5..6d5b78ee9a588 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.113.1", + "version": "1.114.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.113.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index a62e032ef6199..afa5f4585810b 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.113.1", + "version": "1.114.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9e74ae88a00d7..43777552c59bf 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.113.1 + * 1.114.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index ca6a54c82c20e..51f038dfa0301 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.113.1", + "version": "1.114.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.113.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 58d7208adf91f..48e873a8f84c6 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.113.1", + "version": "1.114.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 7bcd5c2b01d47..0fe66f8832e7b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.113.1", + "version": "1.114.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.113.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -74,7 +74,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.113.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index c84bbf0db43d3..4dddc36e41c2e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.113.1", + "version": "1.114.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 8e677ed844592741b8849db09dfcf139ce025b97 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Fri, 6 Sep 2024 19:01:01 +0100 Subject: [PATCH 14/26] ci: tag ml and server images even when they aren't built (#12390) --- .github/workflows/docker.yml | 70 ++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6be26c9bbe62f..8a2ba9f841434 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,6 +40,57 @@ jobs: id: should_force run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" + retag_ml: + name: Re-Tag ML + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_ml == 'false' }} + runs-on: ubuntu-latest + strategy: + matrix: + suffix: ["", "-cuda", "-openvino", "-armnn"] + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + # Skip when PR from a fork + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Re-tag image + run: | + REGISTRY_NAME="ghcr.io" + REPOSITORY=${{ github.repository_owner }}/immich-machine-learning + TAG_OLD=main${{ matrix.suffix }} + TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} + docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD + + retag_server: + name: Re-Tag Server + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_server == 'false' }} + runs-on: ubuntu-latest + strategy: + matrix: + suffix: [""] + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + # Skip when PR from a fork + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Re-tag image + run: | + REGISTRY_NAME="ghcr.io" + REPOSITORY=${{ github.repository_owner }}/immich-server + TAG_OLD=main${{ matrix.suffix }} + TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} + docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD + + build_and_push_ml: name: Build and Push ML needs: pre-job @@ -235,9 +286,22 @@ jobs: BUILD_SOURCE_REF=${{ github.ref_name }} BUILD_SOURCE_COMMIT=${{ github.sha }} - success-check: - name: Docker Build & Push Success - needs: [build_and_push_ml, build_and_push_server] + success-check-server: + name: Docker Build & Push Server Success + needs: [build_and_push_server, retag_server] + runs-on: ubuntu-latest + if: always() + steps: + - name: Any jobs failed? + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 + - name: All jobs passed or skipped + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" + + success-check-ml: + name: Docker Build & Push ML Success + needs: [build_and_push_ml, retag_ml] runs-on: ubuntu-latest if: always() steps: From 7bcef37ba70837008c7b2e613de8386bc029ae41 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 6 Sep 2024 15:13:17 -0400 Subject: [PATCH 15/26] chore: auto-label translations (#12404) --- .github/labeler.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index a0eec41346a68..2a9abc7840381 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -33,3 +33,8 @@ documentation: - changed-files: - any-glob-to-any-file: - machine-learning/app/** + +changelog:translation: + - changed-files: + - any-glob-to-any-file: + - web/src/lib/i18n/*.json From 8f73313b2360377fd1a13e0a380ea6b559fadbce Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sat, 7 Sep 2024 15:14:59 +0200 Subject: [PATCH 16/26] docs: update public sharing support in README feature table (#12437) Closes #8205 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85854577079f7..44c38e6d14813 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ For the mobile app, you can use `https://demo.immich.app/api` for the `Server En | LivePhoto/MotionPhoto backup and playback | Yes | Yes | | Support 360 degree image display | No | Yes | | User-defined storage structure | Yes | Yes | -| Public Sharing | No | Yes | +| Public Sharing | Yes | Yes | | Archive and Favorites | Yes | Yes | | Global Map | Yes | Yes | | Partner Sharing | Yes | Yes | From 5fc3cb556730d9757307bf846d3ffcb60584fc35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:19:33 -0400 Subject: [PATCH 17/26] chore(deps): update docker.io/redis:6.2-alpine docker digest to d72905e (#12422) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 927a95f5274c5..2f7d41271dcdd 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -48,7 +48,7 @@ services: redis: container_name: immich_redis - image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e + image: docker.io/redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd healthcheck: test: redis-cli ping || exit 1 restart: always From 0dabb890cfc550b2a2e6efd630ed85df02777643 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:20:00 -0400 Subject: [PATCH 18/26] chore(deps): update redis:6.2-alpine docker digest to d72905e (#12423) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.dev.yml | 2 +- docker/docker-compose.prod.yml | 2 +- e2e/docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 831b308a0c387..16ed032dfb1c1 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -98,7 +98,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e + image: redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd healthcheck: test: redis-cli ping || exit 1 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 509674f328b35..3e62e7f5619ab 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -47,7 +47,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e + image: redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index cbeca0deca296..dd7632b212fe3 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -33,7 +33,7 @@ services: - 2285:3001 redis: - image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e + image: redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd database: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 From 2554cc96b0e0d65fe0a2c34318fc6e0f5c97020b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 7 Sep 2024 13:21:05 -0400 Subject: [PATCH 19/26] feat(web): logout of all tabs (#12407) --- server/src/interfaces/event.interface.ts | 7 +++++- server/src/repositories/event.repository.ts | 12 ++++++---- server/src/services/auth.service.spec.ts | 7 +++++- server/src/services/auth.service.ts | 3 +++ .../src/services/notification.service.spec.ts | 15 +++++++++++- server/src/services/notification.service.ts | 9 ++++++- .../navigation-bar/navigation-bar.svelte | 24 +++++++------------ web/src/lib/stores/websocket.ts | 4 ++++ web/src/lib/utils/auth.ts | 17 ++++++++++++- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index bb2b0d9ab4bc9..ec6e776f5992b 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -21,6 +21,9 @@ type EmitEventMap = { 'asset.tag': [{ assetId: string }]; 'asset.untag': [{ assetId: string }]; + // session events + 'session.delete': [{ sessionId: string }]; + // user events 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; @@ -43,6 +46,7 @@ export enum ClientEvent { SERVER_VERSION = 'on_server_version', CONFIG_UPDATE = 'on_config_update', NEW_RELEASE = 'on_new_release', + SESSION_DELETE = 'on_session_delete', } export interface ClientEventMap { @@ -58,6 +62,7 @@ export interface ClientEventMap { [ClientEvent.SERVER_VERSION]: ServerVersionResponseDto; [ClientEvent.CONFIG_UPDATE]: Record; [ClientEvent.NEW_RELEASE]: ReleaseNotification; + [ClientEvent.SESSION_DELETE]: string; } export enum ServerEvent { @@ -77,7 +82,7 @@ export interface IEventRepository { /** * Send to connected clients for a specific user */ - clientSend(event: E, userId: string, data: ClientEventMap[E]): void; + clientSend(event: E, room: string, data: ClientEventMap[E]): void; /** * Send to all connected clients */ diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 668eac48d9de9..9aa12e15dd560 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { OnGatewayConnection, @@ -37,7 +38,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect private server?: Server; constructor( - private authService: AuthService, + private moduleRef: ModuleRef, private eventEmitter: EventEmitter2, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { @@ -62,12 +63,15 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect async handleConnection(client: Socket) { try { this.logger.log(`Websocket Connect: ${client.id}`); - const auth = await this.authService.authenticate({ + const auth = await this.moduleRef.get(AuthService).authenticate({ headers: client.request.headers, queryParams: {}, metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' }, }); await client.join(auth.user.id); + if (auth.session) { + await client.join(auth.session.id); + } this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id }); } catch (error: Error | any) { this.logger.error(`Websocket connection error: ${error}`, error?.stack); @@ -96,8 +100,8 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect } } - clientSend(event: E, userId: string, data: ClientEventMap[E]) { - this.server?.to(userId).emit(event, data); + clientSend(event: E, room: string, data: ClientEventMap[E]) { + this.server?.to(room).emit(event, data); } clientBroadcast(event: E, data: ClientEventMap[E]) { diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index f2fa0c520a30f..acc2d3459ccd1 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -6,6 +6,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; @@ -20,6 +21,7 @@ import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; @@ -56,6 +58,7 @@ const oauthUserWithDefaultQuota = { describe('AuthService', () => { let sut: AuthService; let cryptoMock: Mocked; + let eventMock: Mocked; let userMock: Mocked; let loggerMock: Mocked; let systemMock: Mocked; @@ -87,6 +90,7 @@ describe('AuthService', () => { } as any); cryptoMock = newCryptoRepositoryMock(); + eventMock = newEventRepositoryMock(); userMock = newUserRepositoryMock(); loggerMock = newLoggerRepositoryMock(); systemMock = newSystemMetadataRepositoryMock(); @@ -94,7 +98,7 @@ describe('AuthService', () => { shareMock = newSharedLinkRepositoryMock(); keyMock = newKeyRepositoryMock(); - sut = new AuthService(cryptoMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock); + sut = new AuthService(cryptoMock, eventMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock); }); it('should be defined', () => { @@ -208,6 +212,7 @@ describe('AuthService', () => { }); expect(sessionMock.delete).toHaveBeenCalledWith('token123'); + expect(eventMock.emit).toHaveBeenCalledWith('session.delete', { sessionId: 'token123' }); }); it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 2b25decc07035..6eaf755d0eb49 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -34,6 +34,7 @@ import { UserEntity } from 'src/entities/user.entity'; import { Permission } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; @@ -75,6 +76,7 @@ export class AuthService { constructor( @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @@ -114,6 +116,7 @@ export class AuthService { async logout(auth: AuthDto, authType: AuthType): Promise { if (auth.session) { await this.sessionRepository.delete(auth.session.id); + await this.eventRepository.emit('session.delete', { sessionId: auth.session.id }); } return { diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 5bcead0ff31ae..9d9f8f5fcfe2f 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -6,6 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; @@ -17,6 +18,7 @@ import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock'; @@ -64,6 +66,7 @@ const configs = { describe(NotificationService.name, () => { let albumMock: Mocked; let assetMock: Mocked; + let eventMock: Mocked; let jobMock: Mocked; let loggerMock: Mocked; let notificationMock: Mocked; @@ -74,13 +77,23 @@ describe(NotificationService.name, () => { beforeEach(() => { albumMock = newAlbumRepositoryMock(); assetMock = newAssetRepositoryMock(); + eventMock = newEventRepositoryMock(); jobMock = newJobRepositoryMock(); loggerMock = newLoggerRepositoryMock(); notificationMock = newNotificationRepositoryMock(); systemMock = newSystemMetadataRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new NotificationService(systemMock, notificationMock, userMock, jobMock, loggerMock, assetMock, albumMock); + sut = new NotificationService( + eventMock, + systemMock, + notificationMock, + userMock, + jobMock, + loggerMock, + assetMock, + albumMock, + ); }); it('should work', () => { diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 274c91661ca2b..d450f8dc759a2 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -6,7 +6,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ArgOf } from 'src/interfaces/event.interface'; +import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IEmailJob, IJobRepository, @@ -30,6 +30,7 @@ export class NotificationService { private configCore: SystemConfigCore; constructor( + @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(INotificationRepository) private notificationRepository: INotificationRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @@ -74,6 +75,12 @@ export class NotificationService { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } }); } + @OnEmit({ event: 'session.delete' }) + onSessionDelete({ sessionId }: ArgOf<'session.delete'>) { + // after the response is sent + setTimeout(() => this.eventRepository.clientSend(ClientEvent.SESSION_DELETE, sessionId, sessionId), 500); + } + async sendTestEmail(id: string, dto: SystemConfigSmtpDto) { const user = await this.userRepository.get(id, { withDeleted: false }); if (!user) { diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 044a81b222d67..ad8801ff3f8bf 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -1,15 +1,17 @@ @@ -153,7 +145,7 @@ {/if} {#if shouldShowAccountInfoPanel} - + {/if}
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 6422983d94f8a..d398ca52a9d89 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -1,3 +1,5 @@ +import { AppRoute } from '$lib/constants'; +import { handleLogout } from '$lib/utils/auth'; import { createEventEmitter } from '$lib/utils/eventemitter'; import type { AssetResponseDto, ServerVersionResponseDto } from '@immich/sdk'; import { io, type Socket } from 'socket.io-client'; @@ -24,6 +26,7 @@ export interface Events { on_server_version: (serverVersion: ServerVersionResponseDto) => void; on_config_update: () => void; on_new_release: (newRelase: ReleaseEvent) => void; + on_session_delete: (sessionId: string) => void; } const websocket: Socket = io({ @@ -47,6 +50,7 @@ websocket .on('disconnect', () => websocketStore.connected.set(false)) .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion)) .on('on_new_release', (releaseVersion) => websocketStore.release.set(releaseVersion)) + .on('on_session_delete', () => handleLogout(AppRoute.AUTH_LOGIN)) .on('connect_error', (e) => console.log('Websocket Connect Error', e)); export const openWebsocketConnection = () => { diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index d37f1bb96074d..0ac1658948fb6 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,7 +1,9 @@ import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import { foldersStore } from '$lib/stores/folders.store'; import { purchaseStore } from '$lib/stores/purchase.store'; import { serverInfo } from '$lib/stores/server-info.store'; -import { preferences as preferences$, user as user$ } from '$lib/stores/user.store'; +import { preferences as preferences$, resetSavedUser, user as user$ } from '$lib/stores/user.store'; import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import { DateTime } from 'luxon'; @@ -87,3 +89,16 @@ export const getAccountAge = (): number => { return Number(accountAge); }; + +export const handleLogout = async (redirectUri: string) => { + try { + if (redirectUri.startsWith('/')) { + await goto(redirectUri); + } else { + window.location.href = redirectUri; + } + } finally { + resetSavedUser(); + foldersStore.clearCache(); + } +}; From 1e3052bd0bdaad65150f0c71fc1ea8de46db8750 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 7 Sep 2024 13:21:25 -0400 Subject: [PATCH 20/26] feat(server): start up folder checks (#12401) --- server/src/entities/system-metadata.entity.ts | 10 +-- server/src/enum.ts | 1 + server/src/interfaces/database.interface.ts | 1 + server/src/main.ts | 6 ++ server/src/services/storage.service.spec.ts | 42 ++++++++++- server/src/services/storage.service.ts | 69 ++++++++++++++++++- server/src/utils/events.ts | 3 + server/src/workers/api.ts | 8 ++- server/src/workers/microservices.ts | 7 +- 9 files changed, 133 insertions(+), 14 deletions(-) diff --git a/server/src/entities/system-metadata.entity.ts b/server/src/entities/system-metadata.entity.ts index ae01c47b846d9..0a238e1da5f6b 100644 --- a/server/src/entities/system-metadata.entity.ts +++ b/server/src/entities/system-metadata.entity.ts @@ -12,12 +12,14 @@ export class SystemMetadataEntity> { - [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; - [SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string }; [SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean }; - [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial; - [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata; + [SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string }; [SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; + [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; + [SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial; + [SystemMetadataKey.SYSTEM_FLAGS]: SystemFlags; + [SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata; } diff --git a/server/src/enum.ts b/server/src/enum.ts index 28973e0205831..32254854e4c5a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -153,6 +153,7 @@ export enum SystemMetadataKey { FACIAL_RECOGNITION_STATE = 'facial-recognition-state', ADMIN_ONBOARDING = 'admin-onboarding', SYSTEM_CONFIG = 'system-config', + SYSTEM_FLAGS = 'system-flags', VERSION_CHECK_STATE = 'version-check-state', LICENSE = 'license', } diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index 373f1091429d7..51b39b95a8c08 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -15,6 +15,7 @@ export enum VectorIndex { export enum DatabaseLock { GeodataImport = 100, Migrations = 200, + SystemFileMounts = 300, StorageTemplateMigration = 420, CLIPDimSize = 512, LibraryWatch = 1337, diff --git a/server/src/main.ts b/server/src/main.ts index 7839bafd2fa78..ee4de1a259d19 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -17,7 +17,13 @@ async function bootstrapImmichAdmin() { function bootstrapWorker(name: string) { console.log(`Starting ${name} worker`); + const worker = name === 'api' ? fork(`./dist/workers/${name}.js`) : new Worker(`./dist/workers/${name}.js`); + + worker.on('error', (error) => { + console.error(`${name} worker error: ${error}`); + }); + worker.on('exit', (exitCode) => { if (exitCode !== 0) { console.error(`${name} worker exited with code ${exitCode}`); diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index d9b4c8eefb3f3..b0f38554cb032 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -1,19 +1,29 @@ +import { SystemMetadataKey } from 'src/enum'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { StorageService } from 'src/services/storage.service'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { Mocked } from 'vitest'; describe(StorageService.name, () => { let sut: StorageService; + let databaseMock: Mocked; let storageMock: Mocked; let loggerMock: Mocked; + let systemMock: Mocked; beforeEach(() => { + databaseMock = newDatabaseRepositoryMock(); storageMock = newStorageRepositoryMock(); loggerMock = newLoggerRepositoryMock(); - sut = new StorageService(storageMock, loggerMock); + systemMock = newSystemMetadataRepositoryMock(); + + sut = new StorageService(databaseMock, storageMock, loggerMock, systemMock); }); it('should work', () => { @@ -21,9 +31,35 @@ describe(StorageService.name, () => { }); describe('onBootstrap', () => { - it('should create the library folder on initialization', () => { - sut.onBootstrap(); + it('should enable mount folder checking', async () => { + systemMock.get.mockResolvedValue(null); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, { mountFiles: true }); + expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/encoded-video'); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library'); + expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile'); + expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs'); + }); + + it('should throw an error if .immich is missing', async () => { + systemMock.get.mockResolvedValue({ mountFiles: true }); + storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); + + await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount'); + + expect(storageMock.writeFile).not.toHaveBeenCalled(); + expect(systemMock.set).not.toHaveBeenCalled(); + }); + + it('should throw an error if .immich is present but read-only', async () => { + systemMock.get.mockResolvedValue({ mountFiles: true }); + storageMock.writeFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); + + await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount'); + + expect(systemMock.set).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index c3f2c06438340..a8f6a76e747e1 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,23 +1,52 @@ import { Inject, Injectable } from '@nestjs/common'; +import { join } from 'node:path'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { OnEmit } from 'src/decorators'; +import { SystemMetadataKey } from 'src/enum'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ImmichStartupError } from 'src/utils/events'; @Injectable() export class StorageService { constructor( + @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ISystemMetadataRepository) private systemMetadata: ISystemMetadataRepository, ) { this.logger.setContext(StorageService.name); } @OnEmit({ event: 'app.bootstrap' }) - onBootstrap() { - const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); - this.storageRepository.mkdirSync(libraryBase); + async onBootstrap() { + await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { + const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false }; + + this.logger.log('Verifying system mount folder checks'); + + // check each folder exists and is writable + for (const folder of Object.values(StorageFolder)) { + if (!flags.mountFiles) { + this.logger.log(`Writing initial mount file for the ${folder} folder`); + await this.verifyWriteAccess(folder); + } + + await this.verifyReadAccess(folder); + await this.verifyWriteAccess(folder); + } + + if (!flags.mountFiles) { + flags.mountFiles = true; + await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags); + this.logger.log('Successfully enabled system mount folders checks'); + } + + this.logger.log('Successfully verified system mount folder checks'); + }); } async handleDeleteFiles(job: IDeleteFilesJob) { @@ -38,4 +67,38 @@ export class StorageService { return JobStatus.SUCCESS; } + + private async verifyReadAccess(folder: StorageFolder) { + const { filePath } = this.getMountFilePaths(folder); + try { + await this.storageRepository.readFile(filePath); + } catch (error) { + this.logger.error(`Failed to read ${filePath}: ${error}`); + this.logger.error( + `The "${folder}" folder appears to be offline/missing, please make sure the volume is mounted with the correct permissions`, + ); + throw new ImmichStartupError(`Failed to validate folder mount (read from "/${folder}")`); + } + } + + private async verifyWriteAccess(folder: StorageFolder) { + const { folderPath, filePath } = this.getMountFilePaths(folder); + try { + this.storageRepository.mkdirSync(folderPath); + await this.storageRepository.writeFile(filePath, Buffer.from(`${Date.now()}`)); + } catch (error) { + this.logger.error(`Failed to write ${filePath}: ${error}`); + this.logger.error( + `The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`, + ); + throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); + } + } + + private getMountFilePaths(folder: StorageFolder) { + const folderPath = StorageCore.getBaseFolder(folder); + const filePath = join(folderPath, '.immich'); + + return { folderPath, filePath }; + } } diff --git a/server/src/utils/events.ts b/server/src/utils/events.ts index 2dd7e7fd5d208..064c9f75071ef 100644 --- a/server/src/utils/events.ts +++ b/server/src/utils/events.ts @@ -12,6 +12,9 @@ type Item = { label: string; }; +export class ImmichStartupError extends Error {} +export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError; + export const setupEventHandlers = (moduleRef: ModuleRef) => { const reflector = moduleRef.get(Reflector, { strict: false }); const repository = moduleRef.get(IEventRepository); diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index 5857f587a0d2e..629c50c653003 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -9,6 +9,7 @@ import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { ApiService } from 'src/services/api.service'; +import { isStartUpError } from 'src/utils/events'; import { otelStart } from 'src/utils/instrumentation'; import { useSwagger } from 'src/utils/misc'; @@ -73,6 +74,9 @@ async function bootstrap() { } bootstrap().catch((error) => { - console.error(error); - throw error; + if (!isStartUpError(error)) { + console.error(error); + } + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); }); diff --git a/server/src/workers/microservices.ts b/server/src/workers/microservices.ts index f920e8c9476ea..789b6f5287bbd 100644 --- a/server/src/workers/microservices.ts +++ b/server/src/workers/microservices.ts @@ -4,6 +4,7 @@ import { MicroservicesModule } from 'src/app.module'; import { envName, serverVersion } from 'src/constants'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; +import { isStartUpError } from 'src/utils/events'; import { otelStart } from 'src/utils/instrumentation'; export async function bootstrap() { @@ -25,7 +26,9 @@ export async function bootstrap() { if (!isMainThread) { bootstrap().catch((error) => { - console.error(error); - process.exit(1); + if (!isStartUpError(error)) { + console.error(error); + } + throw error; }); } From 00a5da0ebc30c74657cf70e3b008f4e227f430fe Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Sep 2024 12:26:18 -0500 Subject: [PATCH 21/26] chore(mobile): post release task (#12398) Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index fb96609a06047..2d8439e36a4f3 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -401,7 +401,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 172; + CURRENT_PROJECT_VERSION = 173; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 172; + CURRENT_PROJECT_VERSION = 173; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -571,7 +571,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 172; + CURRENT_PROJECT_VERSION = 173; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 138b0e426d251..b33be9a370df7 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.113.1 + 1.114.0 CFBundleSignature ???? CFBundleVersion - 172 + 173 FLTEnableImpeller ITSAppUsesNonExemptEncryption From a9caa407ec521870caa1cad43c1d5d39622d4422 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Sat, 7 Sep 2024 13:39:10 -0400 Subject: [PATCH 22/26] refactor: metadata extraction (#12359) --- server/src/interfaces/map.interface.ts | 2 +- server/src/interfaces/metadata.interface.ts | 2 +- server/src/repositories/map.repository.ts | 4 +- .../src/repositories/metadata.repository.ts | 6 +- server/src/services/metadata.service.spec.ts | 7 +- server/src/services/metadata.service.ts | 263 +++++++++--------- 6 files changed, 146 insertions(+), 138 deletions(-) diff --git a/server/src/interfaces/map.interface.ts b/server/src/interfaces/map.interface.ts index dce75ffd25b03..80b37c3a5f182 100644 --- a/server/src/interfaces/map.interface.ts +++ b/server/src/interfaces/map.interface.ts @@ -26,7 +26,7 @@ export interface MapMarker extends ReverseGeocodeResult { export interface IMapRepository { init(): Promise; - reverseGeocode(point: GeoPoint): Promise; + reverseGeocode(point: GeoPoint): Promise; getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise; fetchStyle(url: string): Promise; } diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts index 04e7b89d1e138..39ff6ab4af33d 100644 --- a/server/src/interfaces/metadata.interface.ts +++ b/server/src/interfaces/metadata.interface.ts @@ -50,7 +50,7 @@ export interface ImmichTags extends Omit { export interface IMetadataRepository { teardown(): Promise; - readTags(path: string): Promise; + readTags(path: string): Promise; writeTags(path: string, tags: Partial): Promise; extractBinaryTag(tagName: string, path: string): Promise; getCountries(userIds: string[]): Promise>; diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index da4e30d47cbf8..3508de720b2e1 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -124,7 +124,7 @@ export class MapRepository implements IMapRepository { } } - async reverseGeocode(point: GeoPoint): Promise { + async reverseGeocode(point: GeoPoint): Promise { this.logger.debug(`Request: ${point.latitude},${point.longitude}`); const response = await this.geodataPlacesRepository @@ -159,7 +159,7 @@ export class MapRepository implements IMapRepository { `Response from database for natural earth reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`, ); - return null; + return { country: null, state: null, city: null }; } this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`); diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index abffc1b78527a..9902f04d9bfcf 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -36,11 +36,11 @@ export class MetadataRepository implements IMetadataRepository { await this.exiftool.end(); } - readTags(path: string): Promise { + readTags(path: string): Promise { return this.exiftool.read(path).catch((error) => { this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack); - return null; - }) as Promise; + return {}; + }) as Promise; } extractBinaryTag(path: string, tagName: string): Promise { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 52f6609772b9d..5b447c235539e 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -522,13 +522,13 @@ describe(MetadataService.name, () => { it('should extract the correct video orientation', async () => { assetMock.getByIds.mockResolvedValue([assetStub.video]); mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); - metadataMock.readTags.mockResolvedValue(null); + metadataMock.readTags.mockResolvedValue({}); await sut.handleMetadataExtraction({ id: assetStub.video.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalledWith( - expect.objectContaining({ orientation: Orientation.Rotate270CW }), + expect.objectContaining({ orientation: Orientation.Rotate270CW.toString() }), ); }); @@ -814,6 +814,9 @@ describe(MetadataService.name, () => { projectionType: 'EQUIRECTANGULAR', timeZone: tags.tz, rating: tags.Rating, + country: null, + state: null, + city: null, }); expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 58e7b994480ac..cf51a332f844c 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ContainerDirectoryItem, ExifDateTime, Tags } from 'exiftool-vendored'; +import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import _ from 'lodash'; import { Duration } from 'luxon'; @@ -11,7 +11,6 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { ExifEntity } from 'src/entities/exif.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetType, SourceType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; @@ -30,7 +29,7 @@ import { QueueName, } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; +import { IMapRepository, ReverseGeocodeResult } from 'src/interfaces/map.interface'; import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; @@ -56,23 +55,16 @@ const EXIF_DATE_TAGS: Array = [ ]; export enum Orientation { - Horizontal = '1', - MirrorHorizontal = '2', - Rotate180 = '3', - MirrorVertical = '4', - MirrorHorizontalRotate270CW = '5', - Rotate90CW = '6', - MirrorHorizontalRotate90CW = '7', - Rotate270CW = '8', + Horizontal = 1, + MirrorHorizontal = 2, + Rotate180 = 3, + MirrorVertical = 4, + MirrorHorizontalRotate270CW = 5, + Rotate90CW = 6, + MirrorHorizontalRotate90CW = 7, + Rotate270CW = 8, } -type ExifEntityWithoutGeocodeAndTypeOrm = Omit & { - dateTimeOriginal: Date; -}; - -const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null); -const tzOffset = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.tzoffsetMinutes : null); - const validate = (value: T): NonNullable | null => { // handle lists of numbers if (Array.isArray(value)) { @@ -218,36 +210,73 @@ export class MetadataService { } async handleMetadataExtraction({ id }: IEntityJob): Promise { - const { metadata } = await this.configCore.getConfig({ withCache: true }); + const { metadata, reverseGeocoding } = await this.configCore.getConfig({ withCache: true }); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return JobStatus.FAILED; } - const { exifData, exifTags } = await this.exifData(asset); + const stats = await this.storageRepository.stat(asset.originalPath); - if (asset.type === AssetType.VIDEO) { - await this.applyVideoMetadata(asset, exifData); - } + const exifTags = await this.getExifTags(asset); + + this.logger.verbose('Exif Tags', exifTags); + + const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags); + const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding); + + const exifData = { + assetId: asset.id, + + // dates + dateTimeOriginal, + modifyDate, + timeZone, + + // gps + latitude, + longitude, + country, + state, + city, + + // image/file + fileSizeInByte: stats.size, + exifImageHeight: validate(exifTags.ImageHeight), + exifImageWidth: validate(exifTags.ImageWidth), + orientation: validate(exifTags.Orientation)?.toString() ?? null, + projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, + bitsPerSample: this.getBitsPerSample(exifTags), + colorspace: exifTags.ColorSpace ?? null, + + // camera + make: exifTags.Make ?? null, + model: exifTags.Model ?? null, + fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), + iso: validate(exifTags.ISO), + exposureTime: exifTags.ExposureTime ?? null, + lensModel: exifTags.LensModel ?? null, + fNumber: validate(exifTags.FNumber), + focalLength: validate(exifTags.FocalLength), + + // comments + description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), + profileDescription: exifTags.ProfileDescription || null, + rating: exifTags.Rating ?? null, + + // grouping + livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, + autoStackId: this.getAutoStackId(exifTags), + }; - await this.applyMotionPhotos(asset, exifTags); - await this.applyReverseGeocoding(asset, exifData); await this.applyTagList(asset, exifTags); + await this.applyMotionPhotos(asset, exifTags); await this.assetRepository.upsertExif(exifData); - const dateTimeOriginal = exifData.dateTimeOriginal; - let localDateTime = dateTimeOriginal ?? undefined; - - const timeZoneOffset = tzOffset(firstDateTime(exifTags as Tags)) ?? 0; - - if (dateTimeOriginal && timeZoneOffset) { - localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000); - } - await this.assetRepository.update({ id: asset.id, - duration: asset.duration, + duration: exifTags.Duration?.toString() ?? null, localDateTime, fileCreatedAt: exifData.dateTimeOriginal ?? undefined, }); @@ -338,25 +367,20 @@ export class MetadataService { return JobStatus.SUCCESS; } - private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { - const { latitude, longitude } = exifData; - const { reverseGeocoding } = await this.configCore.getConfig({ withCache: true }); - if (!reverseGeocoding.enabled || !longitude || !latitude) { - return; + private async getExifTags(asset: AssetEntity): Promise { + const mediaTags = await this.repository.readTags(asset.originalPath); + const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : {}; + const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {}; + + // make sure dates comes from sidecar + const sidecarDate = firstDateTime(sidecarTags as Tags, EXIF_DATE_TAGS); + if (sidecarDate) { + for (const tag of EXIF_DATE_TAGS) { + delete mediaTags[tag]; + } } - try { - const reverseGeocode = await this.mapRepository.reverseGeocode({ latitude, longitude }); - if (!reverseGeocode) { - return; - } - Object.assign(exifData, reverseGeocode); - } catch (error: Error | any) { - this.logger.warn( - `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`, - error?.stack, - ); - } + return { ...mediaTags, ...videoTags, ...sidecarTags }; } private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { @@ -576,66 +600,65 @@ export class MetadataService { ); } - private async exifData( - asset: AssetEntity, - ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; exifTags: ImmichTags }> { - const stats = await this.storageRepository.stat(asset.originalPath); - const mediaTags = await this.repository.readTags(asset.originalPath); - const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null; + private getDates(asset: AssetEntity, exifTags: ImmichTags) { + const dateTime = firstDateTime(exifTags as Maybe, EXIF_DATE_TAGS); + this.logger.debug(`Asset ${asset.id} date time is ${dateTime}`); - // ensure date from sidecar is used if present - const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags); - if (mediaTags && hasDateOverride) { - for (const tag of EXIF_DATE_TAGS) { - delete mediaTags[tag]; - } + // created + let dateTimeOriginal = dateTime?.toDate(); + if (!dateTimeOriginal) { + this.logger.warn(`Asset ${asset.id} has no valid date (${dateTime}), falling back to asset.fileCreatedAt`); + dateTimeOriginal = asset.fileCreatedAt; } - const exifTags = { ...mediaTags, ...sidecarTags }; + // timezone + let timeZone = exifTags.tz ?? null; + if (timeZone == null && dateTime?.rawValue?.endsWith('+00:00')) { + // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly + // https://github.com/photostructure/exiftool-vendored.js/issues/203 + timeZone = 'UTC+0'; + } - this.logger.verbose('Exif Tags', exifTags); + if (timeZone) { + this.logger.debug(`Asset ${asset.id} timezone is ${timeZone} (via ${exifTags.tzSource})`); + } else { + this.logger.warn(`Asset ${asset.id} has no time zone information`); + } - const dateTimeOriginalWithRawValue = this.getDateTimeOriginalWithRawValue(exifTags); - const dateTimeOriginal = dateTimeOriginalWithRawValue.exifDate ?? asset.fileCreatedAt; - const timeZone = this.getTimeZone(exifTags, dateTimeOriginalWithRawValue.rawValue); + // offset minutes + const offsetMinutes = dateTime?.tzoffsetMinutes || 0; + let localDateTime = dateTimeOriginal; + if (offsetMinutes) { + localDateTime = new Date(dateTimeOriginal.getTime() + offsetMinutes * 60_000); + this.logger.debug(`Asset ${asset.id} local time is offset by ${offsetMinutes} minutes`); + } - const exifData = { - // altitude: tags.GPSAltitude ?? null, - assetId: asset.id, - bitsPerSample: this.getBitsPerSample(exifTags), - colorspace: exifTags.ColorSpace ?? null, + return { dateTimeOriginal, - description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), - exifImageHeight: validate(exifTags.ImageHeight), - exifImageWidth: validate(exifTags.ImageWidth), - exposureTime: exifTags.ExposureTime ?? null, - fileSizeInByte: stats.size, - fNumber: validate(exifTags.FNumber), - focalLength: validate(exifTags.FocalLength), - fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), - iso: validate(exifTags.ISO), - latitude: validate(exifTags.GPSLatitude), - lensModel: exifTags.LensModel ?? null, - livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, - autoStackId: this.getAutoStackId(exifTags), - longitude: validate(exifTags.GPSLongitude), - make: exifTags.Make ?? null, - model: exifTags.Model ?? null, - modifyDate: exifDate(exifTags.ModifyDate) ?? asset.fileModifiedAt, - orientation: validate(exifTags.Orientation)?.toString() ?? null, - profileDescription: exifTags.ProfileDescription || null, - projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, timeZone, - rating: exifTags.Rating ?? null, + localDateTime, + modifyDate: (exifTags.ModifyDate as ExifDateTime)?.toDate() ?? asset.fileModifiedAt, }; + } - if (exifData.latitude === 0 && exifData.longitude === 0) { - this.logger.warn('Exif data has latitude and longitude of 0, setting to null'); - exifData.latitude = null; - exifData.longitude = null; + private async getGeo(tags: ImmichTags, reverseGeocoding: SystemConfig['reverseGeocoding']) { + let latitude = validate(tags.GPSLatitude); + let longitude = validate(tags.GPSLongitude); + + // TODO take ref into account + + if (latitude === 0 && longitude === 0) { + this.logger.warn('Latitude and longitude of 0, setting to null'); + latitude = null; + longitude = null; } - return { exifData, exifTags }; + let result: ReverseGeocodeResult = { country: null, state: null, city: null }; + if (reverseGeocoding.enabled && longitude && latitude) { + result = await this.mapRepository.reverseGeocode({ latitude, longitude }); + } + + return { ...result, latitude, longitude }; } private getAutoStackId(tags: ImmichTags | null): string | null { @@ -645,28 +668,6 @@ export class MetadataService { return tags.BurstID ?? tags.BurstUUID ?? tags.CameraBurstID ?? tags.MediaUniqueID ?? null; } - private getDateTimeOriginal(tags: ImmichTags | Tags | null) { - return this.getDateTimeOriginalWithRawValue(tags).exifDate; - } - - private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } { - if (!tags) { - return { exifDate: null, rawValue: '' }; - } - const first = firstDateTime(tags as Tags, EXIF_DATE_TAGS); - return { exifDate: exifDate(first), rawValue: first?.rawValue ?? '' }; - } - - private getTimeZone(exifTags: ImmichTags, rawValue: string) { - const timeZone = exifTags.tz ?? null; - if (timeZone == null && rawValue.endsWith('+00:00')) { - // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly - // https://github.com/photostructure/exiftool-vendored.js/issues/203 - return 'UTC+0'; - } - return timeZone; - } - private getBitsPerSample(tags: ImmichTags): number | null { const bitDepthTags = [ tags.BitsPerSample, @@ -685,33 +686,37 @@ export class MetadataService { return bitsPerSample; } - private async applyVideoMetadata(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { - const { videoStreams, format } = await this.mediaRepository.probe(asset.originalPath); + private async getVideoTags(originalPath: string) { + const { videoStreams, format } = await this.mediaRepository.probe(originalPath); + + const tags: Pick = {}; if (videoStreams[0]) { switch (videoStreams[0].rotation) { case -90: { - exifData.orientation = Orientation.Rotate90CW; + tags.Orientation = Orientation.Rotate90CW; break; } case 0: { - exifData.orientation = Orientation.Horizontal; + tags.Orientation = Orientation.Horizontal; break; } case 90: { - exifData.orientation = Orientation.Rotate270CW; + tags.Orientation = Orientation.Rotate270CW; break; } case 180: { - exifData.orientation = Orientation.Rotate180; + tags.Orientation = Orientation.Rotate180; break; } } } if (format.duration) { - asset.duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS'); + tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS'); } + + return tags; } private async processSidecar(id: string, isSync: boolean): Promise { From 7b1de6209d67b26f440e30026a176cc14a3b71d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:51:48 -0400 Subject: [PATCH 23/26] chore(deps): update docker.io/redis:6.2-alpine docker digest to fd1b540 (#12447) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2f7d41271dcdd..f0590385e4f90 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -48,7 +48,7 @@ services: redis: container_name: immich_redis - image: docker.io/redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd + image: docker.io/redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01 healthcheck: test: redis-cli ping || exit 1 restart: always From 2bf6a46927e2709012974e222b4ddaf8b80437ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:52:04 -0400 Subject: [PATCH 24/26] chore(deps): update redis:6.2-alpine docker digest to fd1b540 (#12448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.dev.yml | 2 +- docker/docker-compose.prod.yml | 2 +- e2e/docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 16ed032dfb1c1..492f5b0c3d6e4 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -98,7 +98,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd + image: redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01 healthcheck: test: redis-cli ping || exit 1 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 3e62e7f5619ab..20126457aa47e 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -47,7 +47,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd + image: redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01 healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index dd7632b212fe3..ce3d1d7ab1334 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -33,7 +33,7 @@ services: - 2285:3001 redis: - image: redis:6.2-alpine@sha256:d72905e7e3d53aa237968c0724f7bb22165c6a2068a06e66a361e4add0df75fd + image: redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01 database: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 From 56bf3cc3d197710e2e034dee9108669f6d721561 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:08:11 -0400 Subject: [PATCH 25/26] chore(ml): bump intel driver version (#12455) update to 24.31.30508.7 --- machine-learning/Dockerfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index f680aac826af3..12fb183c953d4 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -40,11 +40,10 @@ FROM prod-cpu AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ - wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb && \ - wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb && \ - wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb && \ - wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb && \ - wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-core_1.0.17384.11_amd64.deb && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-opencl_1.0.17384.11_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/intel-opencl-icd_24.31.30508.7_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/libigdgmm12_22.4.1_amd64.deb && \ dpkg -i *.deb && \ rm *.deb && \ apt-get remove wget -yqq && \ From d1ce9e4d3c15b73b334e5a2ddbbaafc07ad419cf Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sun, 8 Sep 2024 15:09:27 +0200 Subject: [PATCH 26/26] fix: only apply changelog:translation label to weblate branch (#12468) --- .github/labeler.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 2a9abc7840381..c0c52f1d7e4b9 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -35,6 +35,4 @@ documentation: - machine-learning/app/** changelog:translation: - - changed-files: - - any-glob-to-any-file: - - web/src/lib/i18n/*.json + - head-branch: ['^chore/translations$']