ui changes

This commit is contained in:
Julien Robert 2025-10-14 12:08:51 +02:00
parent 0ee9cc6924
commit feaf95e791
No known key found for this signature in database
GPG key ID: AEA8C21E608F1D02
3 changed files with 122 additions and 8 deletions

View file

@ -1029,6 +1029,7 @@
"unable_to_update_album_info": "Unable to update album info", "unable_to_update_album_info": "Unable to update album info",
"unable_to_update_library": "Unable to update library", "unable_to_update_library": "Unable to update library",
"unable_to_update_location": "Unable to update location", "unable_to_update_location": "Unable to update location",
"unable_to_update_partner": "Unable to update partner",
"unable_to_update_settings": "Unable to update settings", "unable_to_update_settings": "Unable to update settings",
"unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_timeline_display_status": "Unable to update timeline display status",
"unable_to_update_user": "Unable to update user", "unable_to_update_user": "Unable to update user",
@ -1467,6 +1468,11 @@
"partner_page_shared_to_title": "Shared to", "partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{partner} will no longer be able to access your photos.", "partner_page_stop_sharing_content": "{partner} will no longer be able to access your photos.",
"partner_sharing": "Partner Sharing", "partner_sharing": "Partner Sharing",
"partner_sharing_start_date": "Share photos from",
"partner_sharing_start_date_description": "Only photos taken on or after this date will be shared. Leave empty to share all photos.",
"partner_sharing_start_date_help": "Photos taken on or after {date} are shared",
"partner_sharing_start_date_none": "All photos are shared (no date restriction)",
"partner_sharing_start_date_optional": "Optional: Restrict sharing to photos from a specific date",
"partners": "Partners", "partners": "Partners",
"password": "Password", "password": "Password",
"password_does_not_match": "Password does not match", "password_does_not_match": "Password does not match",

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import DateInput from '$lib/elements/DateInput.svelte';
import PartnerSelectionModal from '$lib/modals/PartnerSelectionModal.svelte'; import PartnerSelectionModal from '$lib/modals/PartnerSelectionModal.svelte';
import { import {
createPartner, createPartner,
@ -11,8 +12,8 @@
type PartnerResponseDto, type PartnerResponseDto,
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Button, Icon, IconButton, modalManager } from '@immich/ui'; import { Button, Icon, IconButton, modalManager, Field } from '@immich/ui';
import { mdiCheck, mdiClose } from '@mdi/js'; import { mdiCheck, mdiClose, mdiPencil } from '@mdi/js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
@ -22,6 +23,7 @@
sharedByMe: boolean; sharedByMe: boolean;
sharedWithMe: boolean; sharedWithMe: boolean;
inTimeline: boolean; inTimeline: boolean;
startDate?: string | null;
} }
interface Props { interface Props {
@ -31,6 +33,8 @@
let { user }: Props = $props(); let { user }: Props = $props();
let partners: Array<PartnerSharing> = $state([]); let partners: Array<PartnerSharing> = $state([]);
let editingStartDate: Record<string, boolean> = $state({});
let tempStartDates: Record<string, string | undefined> = $state({});
onMount(async () => { onMount(async () => {
await refreshPartners(); await refreshPartners();
@ -52,6 +56,7 @@
sharedByMe: true, sharedByMe: true,
sharedWithMe: false, sharedWithMe: false,
inTimeline: candidate.inTimeline ?? false, inTimeline: candidate.inTimeline ?? false,
startDate: candidate.startDate,
}, },
]; ];
} }
@ -67,11 +72,13 @@
sharedByMe: false, sharedByMe: false,
sharedWithMe: true, sharedWithMe: true,
inTimeline: candidate.inTimeline ?? false, inTimeline: candidate.inTimeline ?? false,
startDate: candidate.startDate,
}, },
]; ];
} else { } else {
partners[existIndex].sharedWithMe = true; partners[existIndex].sharedWithMe = true;
partners[existIndex].inTimeline = candidate.inTimeline ?? false; partners[existIndex].inTimeline = candidate.inTimeline ?? false;
partners[existIndex].startDate = candidate.startDate;
} }
} }
}; };
@ -95,15 +102,26 @@
}; };
const handleCreatePartners = async () => { const handleCreatePartners = async () => {
const users = await modalManager.show(PartnerSelectionModal, { user }); const result = await modalManager.show(PartnerSelectionModal, { user });
if (!result) {
return;
}
const [users, startDate] = result;
if (!users) { if (!users) {
return; return;
} }
try { try {
for (const user of users) { for (const selectedUser of users) {
await createPartner({ partnerCreateDto: { sharedWithId: user.id } }); await createPartner({
partnerCreateDto: {
sharedWithId: selectedUser.id,
startDate: startDate || undefined,
}
});
} }
await refreshPartners(); await refreshPartners();
@ -121,6 +139,43 @@
handleError(error, $t('errors.unable_to_update_timeline_display_status')); handleError(error, $t('errors.unable_to_update_timeline_display_status'));
} }
}; };
const handleEditStartDate = (partnerId: string, currentDate?: string | null) => {
editingStartDate[partnerId] = true;
tempStartDates[partnerId] = currentDate ? currentDate.split('T')[0] : undefined;
};
const handleSaveStartDate = async (partner: PartnerSharing) => {
try {
const newDate = tempStartDates[partner.user.id];
await updatePartner({
id: partner.user.id,
partnerUpdateDto: {
inTimeline: partner.inTimeline,
startDate: newDate || null,
}
});
partner.startDate = newDate || null;
editingStartDate[partner.user.id] = false;
} catch (error) {
handleError(error, $t('errors.unable_to_update_partner'));
}
};
const handleCancelEditStartDate = (partnerId: string) => {
editingStartDate[partnerId] = false;
delete tempStartDates[partnerId];
};
const formatDate = (dateString?: string | null) => {
if (!dateString) return null;
try {
return new Date(dateString).toLocaleDateString();
} catch {
return null;
}
};
</script> </script>
<section class="my-4"> <section class="my-4">
@ -171,6 +226,45 @@
{$t('partner_can_access_location')} {$t('partner_can_access_location')}
</li> </li>
</ul> </ul>
<div class="mt-4">
{#if editingStartDate[partner.user.id]}
<Field label={$t('partner_sharing_start_date')} description={$t('partner_sharing_start_date_description')}>
<div class="flex gap-2 items-center mt-2">
<DateInput
class="immich-form-input flex-1"
id="partner-start-date-{partner.user.id}"
type="date"
bind:value={tempStartDates[partner.user.id]}
/>
<Button size="small" onclick={() => handleSaveStartDate(partner)}>{$t('save')}</Button>
<Button size="small" color="secondary" onclick={() => handleCancelEditStartDate(partner.user.id)}>{$t('cancel')}</Button>
</div>
</Field>
{:else}
<div class="flex items-center justify-between py-2">
<div class="flex-1">
<p class="text-sm font-medium">{$t('partner_sharing_start_date')}</p>
<p class="text-xs text-gray-600 dark:text-gray-400">
{#if partner.startDate}
{$t('partner_sharing_start_date_help', { values: { date: formatDate(partner.startDate) } })}
{:else}
{$t('partner_sharing_start_date_none')}
{/if}
</p>
</div>
<IconButton
shape="round"
color="secondary"
variant="ghost"
onclick={() => handleEditStartDate(partner.user.id, partner.startDate)}
icon={mdiPencil}
size="small"
aria-label={$t('edit')}
/>
</div>
{/if}
</div>
{/if} {/if}
<!-- this user is sharing assets with me --> <!-- this user is sharing assets with me -->

View file

@ -1,19 +1,21 @@
<script lang="ts"> <script lang="ts">
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import DateInput from '$lib/elements/DateInput.svelte';
import { getPartners, PartnerDirection, searchUsers, type UserResponseDto } from '@immich/sdk'; import { getPartners, PartnerDirection, searchUsers, type UserResponseDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { Button, Modal, ModalBody, ModalFooter, Field } from '@immich/ui';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
user: UserResponseDto; user: UserResponseDto;
onClose: (users?: UserResponseDto[]) => void; onClose: (users?: UserResponseDto[], startDate?: string | null) => void;
} }
let { user, onClose }: Props = $props(); let { user, onClose }: Props = $props();
let availableUsers: UserResponseDto[] = $state([]); let availableUsers: UserResponseDto[] = $state([]);
let selectedUsers: UserResponseDto[] = $state([]); let selectedUsers: UserResponseDto[] = $state([]);
let startDate: string | undefined = $state(undefined);
onMount(async () => { onMount(async () => {
let users = await searchUsers(); let users = await searchUsers();
@ -69,9 +71,21 @@
</p> </p>
{/if} {/if}
<div class="mt-4 px-5">
<Field label={$t('partner_sharing_start_date_optional')} description={$t('partner_sharing_start_date_description')}>
<DateInput
class="immich-form-input mt-2"
id="partner-start-date"
type="date"
bind:value={startDate}
placeholder={$t('partner_sharing_start_date')}
/>
</Field>
</div>
<ModalFooter> <ModalFooter>
{#if selectedUsers.length > 0} {#if selectedUsers.length > 0}
<Button shape="round" fullWidth onclick={() => onClose(selectedUsers)}>{$t('add')}</Button> <Button shape="round" fullWidth onclick={() => onClose(selectedUsers, startDate || null)}>{$t('add')}</Button>
{/if} {/if}
</ModalFooter> </ModalFooter>
</div> </div>