mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
Review comments
This commit is contained in:
parent
a72f2d8e0c
commit
c23ca56adb
10 changed files with 80 additions and 112 deletions
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AssetSelectionChangeDateModal from '$lib/modals/AssetSelectionChangeDateModal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||
import { fromTimelinePlainDateTime } from '$lib/utils/timeline-util.js';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
interface Props {
|
||||
|
|
@ -16,37 +13,15 @@
|
|||
let { menuItem = false }: Props = $props();
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const getCurrentInterval = () => {
|
||||
const ids = getSelectedAssets(getOwnedAssets(), $user);
|
||||
const assets = getOwnedAssets().filter((asset) => ids.includes(asset.id));
|
||||
const imageTimestamps = assets.map((asset) => {
|
||||
let localDateTime = fromTimelinePlainDateTime(asset.localDateTime);
|
||||
let fileCreatedAt = fromTimelinePlainDateTime(asset.fileCreatedAt);
|
||||
let offsetMinutes = localDateTime.diff(fileCreatedAt, 'minutes').shiftTo('minutes').minutes;
|
||||
const timeZone = `UTC${offsetMinutes >= 0 ? '+' : ''}${Duration.fromObject({ minutes: offsetMinutes }).toFormat('hh:mm')}`;
|
||||
return fileCreatedAt.setZone('utc', { keepLocalTime: true }).setZone(timeZone);
|
||||
});
|
||||
let minTimestamp = imageTimestamps[0];
|
||||
let maxTimestamp = imageTimestamps[0];
|
||||
for (let current of imageTimestamps) {
|
||||
if (current < minTimestamp) {
|
||||
minTimestamp = current;
|
||||
}
|
||||
|
||||
if (current > maxTimestamp) {
|
||||
maxTimestamp = current;
|
||||
}
|
||||
}
|
||||
return { start: minTimestamp, end: maxTimestamp };
|
||||
};
|
||||
|
||||
const showChangeDate = async () =>
|
||||
await modalManager.show(AssetSelectionChangeDateModal, {
|
||||
const showChangeDate = async () => {
|
||||
const success = await modalManager.show(AssetSelectionChangeDateModal, {
|
||||
initialDate: DateTime.now(),
|
||||
assets: getOwnedAssets(),
|
||||
currentInterval: getCurrentInterval(),
|
||||
clearSelect,
|
||||
});
|
||||
if (success) {
|
||||
clearSelect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if menuItem}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import AssetUpdateDescriptionConfirmModal from '$lib/modals/AssetUpdateDescriptionConfirmModal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAssets } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
const handleUpdateDescription = async () => {
|
||||
const description = await modalManager.show(AssetUpdateDescriptionConfirmModal);
|
||||
if (description) {
|
||||
const ids = getSelectedAssets(getOwnedAssets(), $user);
|
||||
const ids = getOwnedAssetsWithWarning(getOwnedAssets(), $user);
|
||||
|
||||
try {
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, description } });
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAssets } from '@immich/sdk';
|
||||
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const ids = getSelectedAssets(getOwnedAssets(), $user);
|
||||
const ids = getOwnedAssetsWithWarning(getOwnedAssets(), $user);
|
||||
|
||||
try {
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, latitude: point.lat, longitude: point.lng } });
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
||||
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
|
|
@ -22,7 +21,6 @@
|
|||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
interface Props {
|
||||
timelineManager: TimelineManager;
|
||||
|
|
@ -40,6 +38,8 @@
|
|||
scrollToAsset,
|
||||
}: Props = $props();
|
||||
|
||||
const { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
const trashOrDelete = async (force: boolean = false) => {
|
||||
isShowDeleteConfirmation = false;
|
||||
await deleteAssets(
|
||||
|
|
@ -145,11 +145,14 @@
|
|||
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
||||
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
||||
|
||||
const handleOpenDateModal = async () =>
|
||||
await modalManager.show(NavigateToDateModal, {
|
||||
const handleOpenDateModal = async () => {
|
||||
const asset = await modalManager.show(NavigateToDateModal, {
|
||||
timelineManager,
|
||||
onFocusOnAsset: setFocusAsset,
|
||||
});
|
||||
if (asset) {
|
||||
setFocusAsset(asset);
|
||||
}
|
||||
};
|
||||
|
||||
let shortcutList = $derived(
|
||||
(() => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLInputAttributes {
|
||||
type: 'date' | 'datetime-local';
|
||||
value?: string;
|
||||
min?: string;
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@
|
|||
onClose(true);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_change_date'));
|
||||
}
|
||||
onClose(false);
|
||||
}
|
||||
};
|
||||
|
||||
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ describe('DateSelectionModal component', () => {
|
|||
const initialDate = DateTime.fromISO('2024-01-01');
|
||||
const initialTimeZone = 'Europe/Berlin';
|
||||
|
||||
const currentInterval = {
|
||||
start: DateTime.fromISO('2000-02-01T14:00:00+01:00'),
|
||||
end: DateTime.fromISO('2001-02-01T14:00:00+01:00'),
|
||||
};
|
||||
const onClose = vi.fn();
|
||||
|
||||
const getRelativeInputToggle = () => screen.getByTestId('edit-by-offset-switch');
|
||||
|
|
@ -43,7 +39,7 @@ describe('DateSelectionModal component', () => {
|
|||
initialDate,
|
||||
initialTimeZone,
|
||||
assets: [],
|
||||
clearSelect: vitest.fn(),
|
||||
|
||||
onClose,
|
||||
});
|
||||
expect(getDateInput().value).toBe('2024-01-01T00:00');
|
||||
|
|
@ -52,7 +48,7 @@ describe('DateSelectionModal component', () => {
|
|||
|
||||
test('calls onConfirm with correct date on confirm', async () => {
|
||||
render(AssetSelectionChangeDateModal, {
|
||||
props: { initialDate, initialTimeZone, assets: [], clearSelect: vitest.fn(), onClose },
|
||||
props: { initialDate, initialTimeZone, assets: [], onClose },
|
||||
});
|
||||
|
||||
await fireEvent.click(getConfirmButton());
|
||||
|
|
@ -67,7 +63,7 @@ describe('DateSelectionModal component', () => {
|
|||
|
||||
test('calls onCancel on cancel', async () => {
|
||||
render(AssetSelectionChangeDateModal, {
|
||||
props: { initialDate, initialTimeZone, assets: [], clearSelect: vitest.fn(), onClose },
|
||||
props: { initialDate, initialTimeZone, assets: [], onClose },
|
||||
});
|
||||
|
||||
await fireEvent.click(getCancelButton());
|
||||
|
|
@ -83,7 +79,6 @@ describe('DateSelectionModal component', () => {
|
|||
initialDate: dstDate,
|
||||
initialTimeZone,
|
||||
assets: [],
|
||||
clearSelect: vitest.fn(),
|
||||
onClose,
|
||||
});
|
||||
|
||||
|
|
@ -92,7 +87,7 @@ describe('DateSelectionModal component', () => {
|
|||
|
||||
test('calls onConfirm with correct date on confirm', async () => {
|
||||
render(AssetSelectionChangeDateModal, {
|
||||
props: { initialDate: dstDate, initialTimeZone, assets: [], clearSelect: vitest.fn(), onClose },
|
||||
props: { initialDate: dstDate, initialTimeZone, assets: [], onClose },
|
||||
});
|
||||
|
||||
await fireEvent.click(getConfirmButton());
|
||||
|
|
@ -108,7 +103,7 @@ describe('DateSelectionModal component', () => {
|
|||
|
||||
test('calls onConfirm with correct offset in relative mode', async () => {
|
||||
render(AssetSelectionChangeDateModal, {
|
||||
props: { initialDate, initialTimeZone, currentInterval, assets: [], clearSelect: vitest.fn(), onClose },
|
||||
props: { initialDate, initialTimeZone, assets: [], onClose },
|
||||
});
|
||||
|
||||
await fireEvent.click(getRelativeInputToggle());
|
||||
|
|
@ -139,7 +134,7 @@ describe('DateSelectionModal component', () => {
|
|||
test('calls onConfirm with correct timeZone in relative mode', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(AssetSelectionChangeDateModal, {
|
||||
props: { initialDate, initialTimeZone, currentInterval, assets: [], clearSelect: vitest.fn(), onClose },
|
||||
props: { initialDate, initialTimeZone, assets: [], onClose },
|
||||
});
|
||||
|
||||
await user.click(getRelativeInputToggle());
|
||||
|
|
|
|||
|
|
@ -3,38 +3,23 @@
|
|||
import DateInput from '$lib/elements/DateInput.svelte';
|
||||
import DurationInput from '$lib/elements/DurationInput.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import {
|
||||
calcNewDate,
|
||||
getPreferredTimeZone,
|
||||
getTimezones,
|
||||
toIsoDate,
|
||||
type ZoneOption,
|
||||
} from '$lib/modals/timezone-utils';
|
||||
import { getPreferredTimeZone, getTimezones, toIsoDate, type ZoneOption } from '$lib/modals/timezone-utils';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||
import { getOwnedAssetsWithWarning } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateAssets } from '@immich/sdk';
|
||||
import { Button, Field, HStack, Modal, ModalBody, ModalFooter, Switch, VStack } from '@immich/ui';
|
||||
import { mdiCalendarEdit } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
initialDate?: DateTime;
|
||||
initialTimeZone?: string;
|
||||
currentInterval?: { start: DateTime; end: DateTime };
|
||||
assets: TimelineAsset[];
|
||||
clearSelect: () => void;
|
||||
onClose: (success: boolean) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
initialDate = DateTime.now(),
|
||||
initialTimeZone,
|
||||
currentInterval,
|
||||
assets,
|
||||
clearSelect,
|
||||
onClose,
|
||||
}: Props = $props();
|
||||
let { initialDate = DateTime.now(), initialTimeZone, assets, onClose }: Props = $props();
|
||||
|
||||
let showRelative = $state(false);
|
||||
let selectedDuration = $state(0);
|
||||
|
|
@ -46,7 +31,7 @@
|
|||
let selectedOption = $derived(getPreferredTimeZone(initialDate, initialTimeZone, timezones, lastSelectedTimezone));
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const ids = getSelectedAssets(assets, $user);
|
||||
const ids = getOwnedAssetsWithWarning(assets, $user);
|
||||
try {
|
||||
if (showRelative && (selectedDuration || selectedOption)) {
|
||||
await updateAssets({
|
||||
|
|
@ -56,33 +41,33 @@
|
|||
timeZone: selectedOption?.value,
|
||||
},
|
||||
});
|
||||
clearSelect();
|
||||
onClose(true);
|
||||
return;
|
||||
}
|
||||
const isoDate = toIsoDate(selectedDate, selectedOption);
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, dateTimeOriginal: isoDate } });
|
||||
clearSelect();
|
||||
onClose(true);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_change_date'));
|
||||
onClose(false);
|
||||
}
|
||||
};
|
||||
|
||||
let intervalFrom = $derived(
|
||||
currentInterval ? calcNewDate(currentInterval.start, selectedDuration, selectedOption?.value) : undefined,
|
||||
);
|
||||
let intervalTo = $derived(
|
||||
currentInterval ? calcNewDate(currentInterval.end, selectedDuration, selectedOption?.value) : undefined,
|
||||
);
|
||||
// let before = $derived(DateTime.fromObject(assets[0].localDateTime).toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"));
|
||||
|
||||
// let after = $derived(
|
||||
// currentInterval ? calcNewDate(currentInterval.end, selectedDuration, selectedOption?.value) : undefined,
|
||||
// );
|
||||
|
||||
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
|
||||
const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
|
||||
</script>
|
||||
|
||||
<Modal title={$t('edit_date_and_time')} icon={mdiCalendarEdit} onClose={() => onClose(false)} size="large">
|
||||
<Modal title={$t('edit_date_and_time')} icon={mdiCalendarEdit} onClose={() => onClose(false)} size="small">
|
||||
<ModalBody>
|
||||
<VStack fullWidth>
|
||||
<HStack fullWidth>
|
||||
<Field color="muted" label={$t('edit_date_and_time_by_offset')}>
|
||||
<Field color="muted" label="Adjust selection by fixed duration (time zone adjust)">
|
||||
<Switch data-testid="edit-by-offset-switch" bind:checked={showRelative} />
|
||||
</Field>
|
||||
</HStack>
|
||||
|
|
@ -110,17 +95,32 @@
|
|||
onSelect={(option) => (lastSelectedTimezone = option as ZoneOption)}
|
||||
></Combobox>
|
||||
</div>
|
||||
<VStack fullWidth class={!showRelative || !currentInterval ? 'invisible' : ''}>
|
||||
<HStack fullWidth>
|
||||
<div class="immich-form-label" data-testid="interval-preview">{$t('new_date_range')}</div>
|
||||
</HStack>
|
||||
<HStack fullWidth>
|
||||
<label class="immich-form-label" for="from">From</label>
|
||||
<DateInput class="immich-form-input" id="from" type="datetime-local" bind:value={intervalFrom} />
|
||||
<label class="immich-form-label" for="from">To</label>
|
||||
<DateInput class="immich-form-input" id="to" type="datetime-local" bind:value={intervalTo} />
|
||||
</HStack>
|
||||
</VStack>
|
||||
<!-- <Card color="secondary" class={!showRelative || !currentInterval ? 'invisible' : ''}>
|
||||
<CardBody class="p-2">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-3 items-center">
|
||||
<div class="col-span-2 immich-form-label" data-testid="interval-preview">Preview</div>
|
||||
<Text size="small" class="-mt-2 immich-form-label col-span-2"
|
||||
>Showing changes for first selected asset only</Text
|
||||
>
|
||||
<label class="immich-form-label" for="from">Before</label>
|
||||
<DateInput
|
||||
class="dark:text-gray-300 text-gray-700 text-base"
|
||||
id="from"
|
||||
type="datetime-local"
|
||||
readonly
|
||||
bind:value={before}
|
||||
/>
|
||||
<label class="immich-form-label" for="to">After</label>
|
||||
<DateInput
|
||||
class="dark:text-gray-300 text-gray-700 text-base"
|
||||
id="to"
|
||||
type="datetime-local"
|
||||
readonly
|
||||
bind:value={after}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card> -->
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
interface Props {
|
||||
timelineManager: TimelineManager;
|
||||
onFocusOnAsset: (asset: TimelineAsset) => void;
|
||||
onClose: (success: boolean) => void;
|
||||
onClose: (asset?: TimelineAsset) => void;
|
||||
}
|
||||
|
||||
let { timelineManager, onFocusOnAsset, onClose }: Props = $props();
|
||||
let { timelineManager, onClose }: Props = $props();
|
||||
|
||||
const initialDate = DateTime.now();
|
||||
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"));
|
||||
|
|
@ -23,27 +22,21 @@
|
|||
|
||||
const handleClose = async () => {
|
||||
if (!date.isValid || !selectedOption) {
|
||||
onClose(false);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the local date/time components from the selected string using neutral timezone
|
||||
const dateTime = toDatetime(selectedDate, selectedOption);
|
||||
const asset = await timelineManager.getClosestAssetToDate(dateTime);
|
||||
if (asset) {
|
||||
onFocusOnAsset(asset);
|
||||
onClose(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onClose(false);
|
||||
const dateTime = toDatetime(selectedDate, selectedOption) as DateTime<true>;
|
||||
const asset = await timelineManager.getClosestAssetToDate(dateTime.toObject());
|
||||
onClose(asset);
|
||||
};
|
||||
|
||||
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
|
||||
const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
|
||||
</script>
|
||||
|
||||
<Modal title={$t('navigate_to_time')} icon={mdiNavigationVariantOutline} onClose={() => onClose(false)}>
|
||||
<Modal title={$t('navigate_to_time')} icon={mdiNavigationVariantOutline} onClose={() => onClose()}>
|
||||
<ModalBody>
|
||||
<VStack fullWidth>
|
||||
<HStack fullWidth>
|
||||
|
|
@ -61,7 +54,7 @@
|
|||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<HStack fullWidth>
|
||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>{$t('cancel')}</Button>
|
||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
||||
<Button shape="round" type="submit" fullWidth onclick={handleClose}>{$t('confirm')}</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ export const getAssetType = (type: AssetTypeEnum) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getSelectedAssets = (assets: TimelineAsset[], user: UserResponseDto | null): string[] => {
|
||||
export const getOwnedAssetsWithWarning = (assets: TimelineAsset[], user: UserResponseDto | null): string[] => {
|
||||
const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
|
||||
|
||||
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue