diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index 1f03496c78..73ba2f7cc0 100644 --- a/docs/docs/features/mobile-app.mdx +++ b/docs/docs/features/mobile-app.mdx @@ -68,6 +68,56 @@ Now make sure that the local album is selected in the backup screen (steps 1-2 a title="Upload button after photos selection" /> +## Free Up Space + +The **Free Up Space** tool allows you to remove local media files from your device that have already been successfully backed up to your Immich server (and are not in the Immich trash). This helps reclaim storage on your mobile device without losing your memories. + +### How it works + +1. **Configuration:** + - **Cutoff Date:** You can select a cutoff date. The tool will only look for photos and videos **on or before** this date. + - **Filter Options:** You can choose to remove **All** assets, or restrict removal to **Photos only** or **Videos only**. + - **Keep Favorites:** By default, local assets marked as favorites are preserved on your device, even if they match the cutoff date. +2. **Scan & Review:** Before any files are removed, you are presented with a review screen to verify which items will be deleted. +3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin. They will be permanently removed by the OS based on your system settings (usually after 30 days). + +:::info Android Permissions +For the smoothest experience on Android, you should grant Immich special delete privileges. Without this, you may be prompted to confirm deletion for every single image. + +Go to **Immich Settings > Advanced** and enable **"Media Management Access"**. +::: + +### iCloud Photos (iOS Users) + +If you use **iCloud Photos** alongside Immich, it is vital to understand how deletion affects your data. iCloud utilizes a two-way sync; this means deleting a photo from your iPhone to free up space will **also delete it from iCloud**. + +:::warning iCloud & Backups +If you rely on iCloud as a secondary backup (part of a 3-2-1 backup strategy), using the Free Up Space feature in Immich will remove the file from both your phone and iCloud. + +Once deleted, the photo will exist **only** on your Immich server (and your phone's "Recently Deleted" folder for 30 days). + +When you use iCloud Photos and delete a photo or video on one device, it's also deleted on all other devices where you're signed in with the same Apple Account. + +More information on the [Apple Support](https://support.apple.com/en-us/108922#iCloud_photo_library) website + +**Shared Albums** +Assets that are part of an **iCloud Shared Album** are automatically excluded from the cleanup scan to ensure they remain viewable to others in the shared album. +::: + +### External App Dependencies (WhatsApp, etc.) + +:::danger WhatsApp & Local Files +Android applications like **WhatsApp** rely on local files to display media in chat history. + +If Immich backs up your WhatsApp folder and you run **Free Up Space**, the local copies of these images will be deleted. Consequently, **media in your WhatsApp chats will appear blurry or missing.** You will only be able to view these photos inside the Immich app; they will no longer be visible within the WhatsApp interface. + +**Recommendation:** If keeping chat history intact is important, please ensure you review the deletion list carefully or consider excluding WhatsApp folders from the backup if you intend to use this feature frequently. +::: + +:::info reclaim storage +You must empty the system/gallery trash manually to reclaim storage. +::: + ## Album Sync You can sync or mirror an album from your phone to the Immich server on your account. For example, if you select Recents, Camera and Videos album for backup, the corresponding album with the same name will be created on the server. Once the assets from those albums are uploaded, they will be put into the target albums automatically. diff --git a/i18n/en.json b/i18n/en.json index 59a39ded16..fe5935597b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1021,9 +1021,11 @@ "error_getting_places": "Error getting places", "error_loading_image": "Error loading image", "error_loading_partners": "Error loading partners: {error}", + "error_retrieving_asset_information": "Error retrieving asset information", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_title": "Error - Something went wrong", + "error_while_navigating": "Error while navigating to asset", "errors": { "cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset", @@ -1570,7 +1572,7 @@ "no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.", "no_albums_yet": "It looks like you do not have any albums yet.", "no_archived_assets_message": "Archive photos and videos to hide them from your Photos view", - "no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", + "no_assets_message": "Click to upload your first photo", "no_assets_to_show": "No assets to show", "no_cast_devices_found": "No cast devices found", "no_checksum_local": "No checksum available - cannot fetch local assets", @@ -2204,6 +2206,7 @@ "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", + "then": "Then", "they_will_be_merged_together": "They will be merged together", "third_party_resources": "Third-Party Resources", "time": "Time", diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 90c8d7f7d4..19192c9cff 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -285,7 +285,12 @@ class BackgroundUploadService { return null; } - final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + final hasExtension = p.extension(fileName).isNotEmpty; + if (!hasExtension) { + fileName = p.setExtension(fileName, p.extension(asset.name)); + } + final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName; String metadata = UploadTaskMetadata( diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 9cd562b5db..30cf4abcf6 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -315,7 +315,16 @@ class ForegroundUploadService { return; } - final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + + /// Handle special file name from DJI or Fusion app + /// If the file name has no extension, likely due to special renaming template by specific apps + /// we append the original extension from the asset name + final hasExtension = p.extension(fileName).isNotEmpty; + if (!hasExtension) { + fileName = p.setExtension(fileName, p.extension(asset.name)); + } + final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName; final deviceId = Store.get(StoreKey.deviceId); diff --git a/web/src/app.css b/web/src/app.css index 67e943de4f..dc2d3bf3c3 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -4,7 +4,7 @@ /* @import '/usr/ui/dist/theme/default.css'; */ @utility immich-form-input { - @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-100 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; + @apply bg-gray-100 ring-1 ring-gray-200 transition outline-none focus-within:ring-1 disabled:cursor-not-allowed dark:bg-gray-800 dark:ring-neutral-900 flex w-full items-center rounded-lg disabled:bg-gray-300 disabled:text-dark dark:disabled:bg-gray-900 dark:disabled:text-gray-200 flex-1 py-2.5 text-base pl-4 pr-4; } @utility immich-form-label { diff --git a/web/src/lib/components/QueueCard.svelte b/web/src/lib/components/QueueCard.svelte index b98c732348..b7cde7b8f1 100644 --- a/web/src/lib/components/QueueCard.svelte +++ b/web/src/lib/components/QueueCard.svelte @@ -5,6 +5,7 @@ import { Route } from '$lib/route'; import { asQueueItem } from '$lib/services/queue.service'; import { locale } from '$lib/stores/preferences.store'; + import { transformToTitleCase } from '$lib/utils'; import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk'; import { Icon, IconButton, Link } from '@immich/ui'; import { @@ -53,7 +54,7 @@
diff --git a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte index 31e191440e..7018bc5d04 100644 --- a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte +++ b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte @@ -12,7 +12,7 @@ import { handleSystemConfigSave } from '$lib/services/system-config.service'; import { user } from '$lib/stores/user.store'; import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk'; - import { LoadingSpinner } from '@immich/ui'; + import { Heading, LoadingSpinner, Text } from '@immich/ui'; import handlebar from 'handlebars'; import * as luxon from 'luxon'; import { onDestroy } from 'svelte'; @@ -158,7 +158,9 @@ {#if configToEdit.storageTemplate.enabled}
-

{$t('variables')}

+ + {$t('variables')} +
{#await getSupportDateTimeFormat()} @@ -174,11 +176,14 @@
-
-

{$t('template')}

+
+ + + {$t('template')} + -
-

{$t('preview')}

+
+ {$t('preview')}

@@ -249,7 +254,9 @@ {#if !minified}

-

{$t('notes')}

+ + {$t('notes')} +

import { locale } from '$lib/stores/preferences.store'; import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk'; + import { Card, CardBody, CardHeader, Text } from '@immich/ui'; import { DateTime } from 'luxon'; import { t } from 'svelte-i18n'; @@ -15,78 +16,80 @@ }; -

-

{$t('date_and_time')}

-
+{$t('date_and_time')} -
-
-

{$t('admin.storage_template_date_time_description')}

-

{$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T20:03:05.250' } })}

-
-
-
-

{$t('year')}

-
    - {#each options.yearOptions as yearFormat, index (index)} -
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • - {/each} -
-
+ + + {$t('admin.storage_template_date_time_description')} + {$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T20:03:05.250' } })} + + +
+
+ {$t('year')} +
    + {#each options.yearOptions as yearFormat, index (index)} +
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • + {/each} +
+
-
-

{$t('month')}

-
    - {#each options.monthOptions as monthFormat, index (index)} -
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • - {/each} -
-
+
+ {$t('month')} +
    + {#each options.monthOptions as monthFormat, index (index)} +
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • + {/each} +
+
-
-

{$t('week')}

-
    - {#each options.weekOptions as weekFormat, index (index)} -
  • {'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}
  • - {/each} -
-
+
+ {$t('week')} +
    + {#each options.weekOptions as weekFormat, index (index)} +
  • {'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}
  • + {/each} +
+
-
-

{$t('day')}

-
    - {#each options.dayOptions as dayFormat, index (index)} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
+
+ {$t('day')} +
    + {#each options.dayOptions as dayFormat, index (index)} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-

{$t('hour')}

-
    - {#each options.hourOptions as dayFormat, index (index)} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
+
+ {$t('hour')} +
    + {#each options.hourOptions as dayFormat, index (index)} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-

{$t('minute')}

-
    - {#each options.minuteOptions as dayFormat, index (index)} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
-
+
+ {$t('minute')} +
    + {#each options.minuteOptions as dayFormat, index (index)} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-

{$t('second')}

-
    - {#each options.secondOptions as dayFormat, index (index)} -
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • - {/each} -
+
+ {$t('second')} +
    + {#each options.secondOptions as dayFormat, index (index)} +
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • + {/each} +
+
-
-
+ + diff --git a/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte b/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte index 4274ac70bc..f46f33860a 100644 --- a/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte +++ b/web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte @@ -1,56 +1,59 @@ -
-

{$t('other_variables')}

-
+{$t('other_variables')} -
-
-
-

{$t('filename')}

-
    -
  • {`{{filename}}`} - IMG_123
  • -
  • {`{{ext}}`} - jpg
  • -
-
+ + +
+
+ {$t('filename')} +
    +
  • {`{{filename}}`} - IMG_123
  • +
  • {`{{ext}}`} - jpg
  • +
+
-
-

{$t('filetype')}

-
    -
  • {`{{filetype}}`} - VID or IMG
  • -
  • {`{{filetypefull}}`} - VIDEO or IMAGE
  • -
+
+ {$t('filetype')} +
    +
  • {`{{filetype}}`} - VID or IMG
  • +
  • {`{{filetypefull}}`} - VIDEO or IMAGE
  • +
+
+ +
+ {$t('camera')} +
    +
  • {`{{make}}`} - FUJIFILM
  • +
  • {`{{model}}`} - X-T50
  • +
  • {`{{lensModel}}`} - XF27mm F2.8 R WR
  • +
+
+ +
+ {$t('album')} +
    +
  • {`{{album}}`} - Album Name
  • +
  • + {`{{album-startDate-x}}`} - Album Start Date and Time (e.g. album-startDate-yy). + {$t('admin.storage_template_date_time_sample', { values: { date: '2021-12-31T05:32:41.750' } })} +
  • +
  • + {`{{album-endDate-x}}`} - Album End Date and Time (e.g. album-endDate-MM). + {$t('admin.storage_template_date_time_sample', { values: { date: '2023-05-06T09:15:17.100' } })} +
  • +
+
+
+ {$t('other')} +
    +
  • {`{{assetId}}`} - Asset ID
  • +
  • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
  • +
+
-
-

{$t('album')}

-
    -
  • {`{{album}}`} - Album Name
  • -
  • - {`{{album-startDate-x}}`} - Album Start Date and Time (e.g. album-startDate-yy). - {$t('admin.storage_template_date_time_sample', { values: { date: '2021-12-31T05:32:41.750' } })} -
  • -
  • - {`{{album-endDate-x}}`} - Album End Date and Time (e.g. album-endDate-MM). - {$t('admin.storage_template_date_time_sample', { values: { date: '2023-05-06T09:15:17.100' } })} -
  • -
-
-
-

{$t('camera')}

-
    -
  • {`{{make}}`} - FUJIFILM
  • -
  • {`{{model}}`} - X-T50
  • -
  • {`{{lensModel}}`} - XF27mm F2.8 R WR
  • -
-
-
-

{$t('other')}

-
    -
  • {`{{assetId}}`} - Asset ID
  • -
  • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
  • -
-
-
-
+ + diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index f047b5001a..ac9c07df94 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -252,7 +252,7 @@ await handleStopSlideshow(); } } - }); + }, $t('error_while_navigating')); }; const showEditor = () => { diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte index 0e16272cdb..cd9b1a40d2 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -5,7 +5,7 @@ import { Route } from '$lib/route'; import { removeTag } from '$lib/utils/asset-utils'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; - import { Icon, modalManager } from '@immich/ui'; + import { Icon, modalManager, Text } from '@immich/ui'; import { mdiClose, mdiPlus } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -38,7 +38,7 @@ {#if isOwner && !authManager.isSharedLink}
-

{$t('tags')}

+ {$t('tags')}
{#each tags as tag (tag.id)} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 045cc0a0df..e6056dbad2 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -20,7 +20,7 @@ import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { getParentPath } from '$lib/utils/tree-utils'; import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; - import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; + import { Icon, IconButton, LoadingSpinner, modalManager, Text } from '@immich/ui'; import { mdiCalendar, mdiCamera, @@ -160,7 +160,7 @@ {#if !authManager.isSharedLink && isOwner}
-

{$t('people')}

+ {$t('people')}
{#if people.some((person) => person.isHidden)} {#if asset.exifInfo}
-

{$t('details')}

+ {$t('details')}
{:else} -

{$t('no_exif_info_available')}

+ {$t('no_exif_info_available')} {/if} {#if dateTime} @@ -487,7 +487,7 @@ {#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
-

{$t('shared_by')}

+ {$t('shared_by')}
@@ -504,7 +504,9 @@ {#if albums.length > 0}
-

{$t('appears_in')}

+
+ {$t('appears_in')} +
{#each albums as album (album.id)}
diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index 9eb7c78666..ac78623071 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -147,7 +147,7 @@ {/if}
-

{$t('all_people')}

+

{$t('all_people')}

{#if isShowLoadingPeople}
diff --git a/web/src/lib/components/onboarding-page/onboarding-card.svelte b/web/src/lib/components/onboarding-page/onboarding-card.svelte index 0cc9792834..a8463f76ea 100644 --- a/web/src/lib/components/onboarding-page/onboarding-card.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-card.svelte @@ -39,7 +39,7 @@ {/if} {#if title} -

+

{title}

{/if} diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index 9822d042bf..c28c0ee8c7 100644 --- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte @@ -19,7 +19,7 @@ class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary" > -

{$t('light')}

+

{$t('light')}

diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 955ca64565..be1b73e1c5 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -24,7 +24,7 @@ import { shortcuts } from '$lib/actions/shortcut'; import { generateId } from '$lib/utils/generate-id'; import { Icon, IconButton, Label } from '@immich/ui'; - import { mdiClose, mdiMagnify, mdiUnfoldMoreHorizontal } from '@mdi/js'; + import { mdiChevronDown, mdiClose, mdiMagnify } from '@mdi/js'; import { onMount, tick } from 'svelte'; import { t } from 'svelte-i18n'; import type { FormEventHandler } from 'svelte/elements'; @@ -251,7 +251,7 @@ - +
{:else if !isOpen} - + {/if}
@@ -391,7 +391,7 @@
  • handleSelect(option)} role="option" diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index ac158aa8a3..098471c1d2 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -10,6 +10,7 @@ import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte'; import { handlePromiseError } from '$lib/utils'; import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk'; + import { Text } from '@immich/ui'; import { t } from 'svelte-i18n'; interface Props { @@ -81,8 +82,7 @@
    -

    {$t('camera')}

    - + {$t('camera')}
    export interface SearchDateFilter { - takenBefore?: string; - takenAfter?: string; + takenBefore?: DateTime; + takenAfter?: DateTime; }
    - +
    + {$t('start_date')} + +
    - +
    + {$t('end_date')} + +
    {#if invalid} {$t('start_date_before_end_date')} diff --git a/web/src/lib/components/shared-components/search-bar/search-display-section.svelte b/web/src/lib/components/shared-components/search-bar/search-display-section.svelte index 59843d574a..7097ab7a8b 100644 --- a/web/src/lib/components/shared-components/search-bar/search-display-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-display-section.svelte @@ -7,7 +7,7 @@
    -

    {$t('place')}

    + {$t('place')}
    diff --git a/web/src/lib/components/shared-components/search-bar/search-media-section.svelte b/web/src/lib/components/shared-components/search-bar/search-media-section.svelte index 3e05c370c7..297d13033b 100644 --- a/web/src/lib/components/shared-components/search-bar/search-media-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-media-section.svelte @@ -1,6 +1,7 @@ -
    - +
    + {$t('rating')} + (rating = r === undefined ? undefined : Number.parseInt(r.value))} + />
    diff --git a/web/src/lib/components/shared-components/search-bar/search-tags-section.svelte b/web/src/lib/components/shared-components/search-bar/search-tags-section.svelte index f41ef03fdb..208de3f8a4 100644 --- a/web/src/lib/components/shared-components/search-bar/search-tags-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-tags-section.svelte @@ -2,7 +2,7 @@ import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; import { preferences } from '$lib/stores/user.store'; import { getAllTags, type TagResponseDto } from '@immich/sdk'; - import { Checkbox, Icon, Label } from '@immich/ui'; + import { Checkbox, Icon, Label, Text } from '@immich/ui'; import { mdiClose } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; @@ -43,18 +43,18 @@ {#if $preferences?.tags?.enabled}
    -
    -
    - ({ id: tag.id, label: tag.value, value: tag.id }))} - bind:selectedOption - placeholder={$t('search_tags')} - /> -
    +
    + {$t('tags')} + ({ id: tag.id, label: tag.value, value: tag.id }))} + bind:selectedOption + placeholder={$t('search_tags')} + />
    -
    diff --git a/web/src/lib/components/shared-components/search-bar/search-text-section.svelte b/web/src/lib/components/shared-components/search-bar/search-text-section.svelte index d58de23fa8..d16734e13c 100644 --- a/web/src/lib/components/shared-components/search-bar/search-text-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-text-section.svelte @@ -1,6 +1,7 @@ -
    - {$t('search_type')} -
    - {#if featureFlagsManager.value.smartSearch} - - {/if} - - - {#if featureFlagsManager.value.ocr} - - {/if} -
    -
    +
    +
    + {$t('search_type')} +
    + {#if featureFlagsManager.value.smartSearch} + + {/if} + + + {#if featureFlagsManager.value.ocr} + + {/if} +
    +
    -{#if queryType === 'smart'} - - -{:else if queryType === 'metadata'} - - -{:else if queryType === 'description'} - - -{:else if queryType === 'ocr'} - - -{/if} + {#if queryType === 'smart'} + + + + {:else if queryType === 'metadata'} + + + + {:else if queryType === 'description'} + + + + {:else if queryType === 'ocr'} + + + + {/if} +
    diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte index d3e89bb56d..e1c5c2b74a 100644 --- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte +++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte @@ -81,7 +81,7 @@
    - + {#if required}
    *
    {/if} diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index 06ff61d180..f61d88c1c4 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -11,10 +11,12 @@ import { handlePromiseError } from '$lib/utils'; import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { navigateToAsset } from '$lib/utils/asset-utils'; + import { handleErrorAsync } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk'; import { onDestroy, onMount, untrack } from 'svelte'; + import { t } from 'svelte-i18n'; let { asset: viewingAsset, gridScrollTarget } = assetViewingStore; @@ -38,28 +40,27 @@ person, }: Props = $props(); - const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { - const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); - if (earlierTimelineAsset) { - const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id }); - if (preload) { - // also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete - void getNextAsset(asset, false); - } - return asset; - } + const getAsset = (id: string) => { + return handleErrorAsync( + () => assetCacheManager.getAsset({ ...authManager.params, id }), + $t('error_retrieving_asset_information'), + ); }; - const getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => { - const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset); - if (laterTimelineAsset) { - const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id }); - if (preload) { - // also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete - void getPreviousAsset(asset, false); - } - return asset; + const getNextAsset = async (currentAsset: AssetResponseDto) => { + const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset); + if (!earlierTimelineAsset) { + return; } + return getAsset(earlierTimelineAsset.id); + }; + + const getPreviousAsset = async (currentAsset: AssetResponseDto) => { + const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset); + if (!laterTimelineAsset) { + return; + } + return getAsset(laterTimelineAsset.id); }; let assetCursor = $state({ @@ -87,10 +88,12 @@ const handleRandom = async () => { const randomAsset = await timelineManager.getRandomAsset(); - if (randomAsset) { - await navigate({ targetRoute: 'current', assetId: randomAsset.id }); - return { id: randomAsset.id }; + if (!randomAsset) { + return; } + + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + return { id: randomAsset.id }; }; const handleClose = async (asset: { id: string }) => { @@ -180,12 +183,14 @@ }; const handleUndoDelete = async (assets: TimelineAsset[]) => { timelineManager.upsertAssets(assets); - if (assets.length > 0) { - const restoredAsset = assets[0]; - const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); - assetViewingStore.setAsset(asset); - await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); + if (assets.length === 0) { + return; } + + const restoredAsset = assets[0]; + const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); + assetViewingStore.setAsset(asset); + await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); }; const handleUpdateOrUpload = (asset: AssetResponseDto) => { diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index 0789c7b179..41f1e3b790 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -1,7 +1,7 @@
    -

    {$t('organize_your_library')}

    + {$t('organize_your_library')} {#each links as link (link.href)} @@ -33,7 +33,8 @@

    -

    {$t('download')}

    + {$t('download')} +