Merge d88941be7d into 5ade152bc5
commit
8f804e03ba
12
i18n/en.json
12
i18n/en.json
|
|
@ -2047,6 +2047,18 @@
|
|||
"sync_status": "Sync Status",
|
||||
"sync_status_subtitle": "View and manage the sync system",
|
||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||
"synchronize_albums": "Synchronize Albums",
|
||||
"synchronize_albums_description": "Add the resolved asset to every album that any of the duplicates belong to.",
|
||||
"synchronize_description": "Synchronize Description",
|
||||
"synchronize_description_description": "Merge the descriptions from all duplicates into a single description for the resolved asset.",
|
||||
"synchronize_favorites": "Synchronize Favorites",
|
||||
"synchronize_favorites_description": "If any duplicate is marked as a favorite, mark the resolved asset as favorite.",
|
||||
"synchronize_location": "Synchronize Location",
|
||||
"synchronize_location_description": "If exactly one unique latitude/longitude pair exists across the duplicates, apply that location to the resolved asset.",
|
||||
"synchronize_rating": "Synchronize Rating",
|
||||
"synchronize_rating_description": "Use the highest rating found in the duplicates' EXIF data for the resolved asset.",
|
||||
"synchronize_visibility": "Synchronize Visibility setting",
|
||||
"synchronize_visibility_description": "Apply the most restrictive visibility present among the duplicates (Locked → Hidden → Archive → Timeline).",
|
||||
"tag": "Tag",
|
||||
"tag_assets": "Tag assets",
|
||||
"tag_created": "Created tag: {tag}",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
import type { DuplicateSettings } from '$lib/stores/preferences.store';
|
||||
import { Button, Field, HStack, Modal, ModalBody, ModalFooter, Stack, Switch } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
settings: DuplicateSettings;
|
||||
onClose: (settings?: DuplicateSettings) => void;
|
||||
}
|
||||
let { settings: initialValues, onClose }: Props = $props();
|
||||
let settings = $state(initialValues);
|
||||
|
||||
const onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
onClose(settings);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title={$t('options')} {onClose} size="medium">
|
||||
<ModalBody>
|
||||
<form {onsubmit} id="duplicate-settings-form">
|
||||
<Stack gap={4}>
|
||||
<Field label={$t('synchronize_albums')} description={$t('synchronize_albums_description')}>
|
||||
<Switch bind:checked={settings.synchronizeAlbums} />
|
||||
</Field>
|
||||
<Field label={$t('synchronize_favorites')} description={$t('synchronize_favorites_description')}>
|
||||
<Switch bind:checked={settings.synchronizeFavorites} />
|
||||
</Field>
|
||||
<Field label={$t('synchronize_rating')} description={$t('synchronize_rating_description')}>
|
||||
<Switch bind:checked={settings.synchronizeRating} />
|
||||
</Field>
|
||||
<Field label={$t('synchronize_description')} description={$t('synchronize_description_description')}>
|
||||
<Switch bind:checked={settings.synchronizeDescpription} />
|
||||
</Field>
|
||||
<Field label={$t('synchronize_visibility')} description={$t('synchronize_visibility_description')}>
|
||||
<Switch bind:checked={settings.synchronizeVisibility} />
|
||||
</Field>
|
||||
<Field label={$t('synchronize_location')} description={$t('synchronize_location_description')}>
|
||||
<Switch bind:checked={settings.synchronizeLocation} />
|
||||
</Field>
|
||||
</Stack>
|
||||
</form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<HStack fullWidth>
|
||||
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
||||
<Button type="submit" shape="round" fullWidth form="duplicate-settings-form">{$t('save')}</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
@ -151,3 +151,21 @@ export const autoPlayVideo = persisted<boolean>('auto-play-video', true, {});
|
|||
export const alwaysLoadOriginalVideo = persisted<boolean>('always-load-original-video', false, {});
|
||||
|
||||
export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {});
|
||||
|
||||
export interface DuplicateSettings {
|
||||
synchronizeAlbums: boolean;
|
||||
synchronizeVisibility: boolean;
|
||||
synchronizeFavorites: boolean;
|
||||
synchronizeRating: boolean;
|
||||
synchronizeDescpription: boolean;
|
||||
synchronizeLocation: boolean;
|
||||
}
|
||||
|
||||
export const duplicateSettings = persistedObject<DuplicateSettings>('duplicate-settings', {
|
||||
synchronizeAlbums: false,
|
||||
synchronizeVisibility: false,
|
||||
synchronizeFavorites: false,
|
||||
synchronizeRating: false,
|
||||
synchronizeDescpription: false,
|
||||
synchronizeLocation: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,23 +3,26 @@
|
|||
import { page } from '$app/state';
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import DuplicateSettingsModal from '$lib/components/utilities-page/duplicates/duplicate-settings-modal.svelte';
|
||||
import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import DuplicatesInformationModal from '$lib/modals/DuplicatesInformationModal.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { duplicateSettings, locale } from '$lib/stores/preferences.store';
|
||||
import { stackAssets } from '$lib/utils/asset-utils';
|
||||
import { suggestDuplicate } from '$lib/utils/duplicate-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { deleteAssets, deleteDuplicates, updateAssets } from '@immich/sdk';
|
||||
import type { AssetBulkUpdateDto, AssetResponseDto } from '@immich/sdk';
|
||||
import { AssetVisibility, copyAsset, deleteAssets, deleteDuplicates, getAssetInfo, updateAssets } from '@immich/sdk';
|
||||
import { Button, HStack, IconButton, modalManager, Text, toastManager } from '@immich/ui';
|
||||
import {
|
||||
mdiCheckOutline,
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiCogOutline,
|
||||
mdiInformationOutline,
|
||||
mdiKeyboard,
|
||||
mdiPageFirst,
|
||||
|
|
@ -56,6 +59,13 @@
|
|||
],
|
||||
};
|
||||
|
||||
const onShowSettings = async () => {
|
||||
const settings = await modalManager.show(DuplicateSettingsModal, { settings: { ...$duplicateSettings } });
|
||||
if (settings) {
|
||||
$duplicateSettings = settings;
|
||||
}
|
||||
};
|
||||
|
||||
let duplicates = $state(data.duplicates);
|
||||
const { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
|
|
@ -98,11 +108,68 @@
|
|||
toastManager.success(message);
|
||||
};
|
||||
|
||||
const getSyncedInfo = async (assetIds: string[]) => {
|
||||
const allAssetsInfo = await Promise.all(
|
||||
assetIds.map((assetId) => getAssetInfo({ ...authManager.params, id: assetId })),
|
||||
);
|
||||
// If any of the assets is favorite, we consider the synced info as favorite
|
||||
const isFavorite = allAssetsInfo.some((asset) => asset.isFavorite);
|
||||
// Choose the most restrictive visibility level among the assets
|
||||
const visibility = [
|
||||
AssetVisibility.Locked,
|
||||
AssetVisibility.Hidden,
|
||||
AssetVisibility.Archive,
|
||||
AssetVisibility.Timeline,
|
||||
].find((level) => allAssetsInfo.some((asset) => asset.visibility === level));
|
||||
// Choose the highest rating from the exif data of the assets
|
||||
const rating = Math.max(...allAssetsInfo.map((asset) => asset.exifInfo?.rating ?? 0));
|
||||
// Concatenate the single descriptions of the assets
|
||||
const description = allAssetsInfo.map((asset) => asset.exifInfo?.description).join('\n');
|
||||
// Check that only one pair of latitude/longitude exists among the assets
|
||||
const latitudes = new Set(allAssetsInfo.map((asset) => asset.exifInfo?.latitude).filter((lat) => lat !== null));
|
||||
const longitudes = new Set(allAssetsInfo.map((asset) => asset.exifInfo?.longitude).filter((lon) => lon !== null));
|
||||
const latitude = latitudes.size === 1 ? Array.from(latitudes)[0] : null;
|
||||
const longitude = longitudes.size === 1 ? Array.from(longitudes)[0] : null;
|
||||
|
||||
return { isFavorite, visibility, rating, description, latitude, longitude };
|
||||
};
|
||||
|
||||
const handleResolve = async (duplicateId: string, duplicateAssetIds: string[], trashIds: string[]) => {
|
||||
return withConfirmation(
|
||||
async () => {
|
||||
const { isFavorite, visibility, rating, description, latitude, longitude } =
|
||||
await getSyncedInfo(duplicateAssetIds);
|
||||
let assetBulkUpdate: AssetBulkUpdateDto = {
|
||||
ids: duplicateAssetIds,
|
||||
duplicateId: null,
|
||||
};
|
||||
if ($duplicateSettings.synchronizeFavorites) {
|
||||
assetBulkUpdate.isFavorite = isFavorite;
|
||||
}
|
||||
if ($duplicateSettings.synchronizeVisibility) {
|
||||
assetBulkUpdate.visibility = visibility;
|
||||
}
|
||||
if ($duplicateSettings.synchronizeRating) {
|
||||
assetBulkUpdate.rating = rating;
|
||||
}
|
||||
if ($duplicateSettings.synchronizeDescpription) {
|
||||
assetBulkUpdate.description = description;
|
||||
}
|
||||
if ($duplicateSettings.synchronizeLocation && latitude !== null && longitude !== null) {
|
||||
assetBulkUpdate.latitude = latitude;
|
||||
assetBulkUpdate.longitude = longitude;
|
||||
}
|
||||
if ($duplicateSettings.synchronizeAlbums) {
|
||||
const idsToKeep = duplicateAssetIds.filter((id) => !trashIds.includes(id));
|
||||
for (const sourceId of trashIds) {
|
||||
for (const targetId of idsToKeep) {
|
||||
await copyAsset({ assetCopyDto: { sourceId, targetId, albums: true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: trashIds, force: !featureFlagsManager.value.trash } });
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
await updateAssets({ assetBulkUpdateDto: assetBulkUpdate });
|
||||
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
|
||||
|
|
@ -245,6 +312,15 @@
|
|||
onclick={() => modalManager.show(ShortcutsModal, { shortcuts: duplicateShortcuts })}
|
||||
aria-label={$t('show_keyboard_shortcuts')}
|
||||
/>
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
icon={mdiCogOutline}
|
||||
title={$t('settings')}
|
||||
onclick={onShowSettings}
|
||||
aria-label={$t('settings')}
|
||||
/>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue