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 @@
@@ -249,7 +254,9 @@ {#if !minified}
{$t('admin.storage_template_date_time_description')} {$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T20:03:05.250' } })} {$t('year')} {$t('month')} {$t('week')} {$t('day')} {$t('hour')} {$t('minute')} {$t('second')} {$t('filename')} {$t('filetype')} {$t('album')} {$t('camera')} {$t('other')} {$t('no_exif_info_available')} {$t('shared_by')} {$t('camera')} {$t('place')}{$t('date_and_time')}
-
- {#each options.yearOptions as yearFormat, index (index)}
-
-
+ {#each options.yearOptions as yearFormat, index (index)}
+
+
- {#each options.monthOptions as monthFormat, index (index)}
-
-
+ {#each options.monthOptions as monthFormat, index (index)}
+
+
- {#each options.weekOptions as weekFormat, index (index)}
-
-
+ {#each options.weekOptions as weekFormat, index (index)}
+
+
- {#each options.dayOptions as dayFormat, index (index)}
-
-
+ {#each options.dayOptions as dayFormat, index (index)}
+
+
- {#each options.hourOptions as dayFormat, index (index)}
-
-
+ {#each options.hourOptions as dayFormat, index (index)}
+
+
- {#each options.minuteOptions as dayFormat, index (index)}
-
-
+ {#each options.minuteOptions as dayFormat, index (index)}
+
+
- {#each options.secondOptions as dayFormat, index (index)}
-
+
+ {#each options.secondOptions as dayFormat, index (index)}
+
+ {$t('other_variables')}
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- {$t('tags')}
+ {$t('people')}
+ {$t('details')}
+