mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(web): show partners assets on the main timeline (#4933)
This commit is contained in:
parent
3b11854702
commit
35767591d2
59 changed files with 1929 additions and 172 deletions
|
|
@ -97,8 +97,12 @@
|
|||
</script>
|
||||
|
||||
<header>
|
||||
{#if $isMultiSelectState}
|
||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
|
||||
{#if $isMultiSelectState && user}
|
||||
<AssetSelectControlBar
|
||||
ownerId={user.id}
|
||||
assets={$selectedAssets}
|
||||
clearSelect={() => assetInteractionStore.clearMultiselect()}
|
||||
>
|
||||
<SelectAllAssets {assetStore} {assetInteractionStore} />
|
||||
{#if sharedLink.allowDownload}
|
||||
<DownloadAction filename="{album.albumName}.zip" />
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@
|
|||
|
||||
let loading = false;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const isArchived = !unarchive;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const assets = Array.from(getAssets()).filter((asset) => asset.isArchived !== isArchived);
|
||||
const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isArchived !== isArchived);
|
||||
const ids = assets.map(({ id }) => id);
|
||||
|
||||
if (ids.length > 0) {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@
|
|||
AssetJobName.TranscodeVideo,
|
||||
];
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
$: isAllVideos = Array.from(getAssets()).every((asset) => asset.type === AssetTypeEnum.Video);
|
||||
$: isAllVideos = Array.from(getOwnedAssets()).every((asset) => asset.type === AssetTypeEnum.Video);
|
||||
|
||||
const handleRunJob = async (name: AssetJobName) => {
|
||||
try {
|
||||
const ids = Array.from(getAssets()).map(({ id }) => id);
|
||||
const ids = Array.from(getOwnedAssets()).map(({ id }) => id);
|
||||
await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
|
||||
notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info });
|
||||
clearSelect();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
export let menuItem = false;
|
||||
export let force = !$featureFlags.trash;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
loading = true;
|
||||
|
||||
try {
|
||||
const ids = Array.from(getAssets())
|
||||
const ids = Array.from(getOwnedAssets())
|
||||
.filter((a) => !a.isExternal)
|
||||
.map((a) => a.id);
|
||||
await api.assetApi.deleteAssets({ assetBulkDeleteDto: { ids, force } });
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
|
||||
{#if isShowConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Permanently Delete Asset{getAssets().size > 1 ? 's' : ''}"
|
||||
title="Permanently Delete Asset{getOwnedAssets().size > 1 ? 's' : ''}"
|
||||
confirmText="Delete"
|
||||
on:confirm={handleDelete}
|
||||
on:cancel={() => (isShowConfirmation = false)}
|
||||
|
|
@ -84,8 +84,8 @@
|
|||
<svelte:fragment slot="prompt">
|
||||
<p>
|
||||
Are you sure you want to permanently delete
|
||||
{#if getAssets().size > 1}
|
||||
these <b>{getAssets().size}</b> assets? This will also remove them from their album(s).
|
||||
{#if getOwnedAssets().size > 1}
|
||||
these <b>{getOwnedAssets().size}</b> assets? This will also remove them from their album(s).
|
||||
{:else}
|
||||
this asset? This will also remove it from its album(s).
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,15 @@
|
|||
|
||||
let loading = false;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const handleFavorite = async () => {
|
||||
const isFavorite = !removeFavorite;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const assets = Array.from(getAssets()).filter((asset) => asset.isFavorite !== isFavorite);
|
||||
const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isFavorite !== isFavorite);
|
||||
|
||||
const ids = assets.map(({ id }) => id);
|
||||
|
||||
if (ids.length > 0) {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
export let onStack: OnStack | undefined = undefined;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const handleStack = async () => {
|
||||
try {
|
||||
const assets = Array.from(getAssets());
|
||||
const assets = Array.from(getOwnedAssets());
|
||||
const parent = assets.at(0);
|
||||
|
||||
if (parent == undefined) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
export interface AssetControlContext {
|
||||
// Wrap assets in a function, because context isn't reactive.
|
||||
getAssets: () => Set<AssetResponseDto>;
|
||||
getAssets: () => Set<AssetResponseDto>; // All assets includes partners' assets
|
||||
getOwnedAssets: () => Set<AssetResponseDto>; // Only assets owned by the user
|
||||
clearSelect: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -25,8 +26,14 @@
|
|||
|
||||
export let assets: Set<AssetResponseDto>;
|
||||
export let clearSelect: () => void;
|
||||
export let ownerId: string | undefined = undefined;
|
||||
|
||||
setContext({ getAssets: () => assets, clearSelect });
|
||||
setContext({
|
||||
getAssets: () => assets,
|
||||
getOwnedAssets: () =>
|
||||
ownerId !== undefined ? new Set(Array.from(assets).filter((asset) => asset.ownerId === ownerId)) : assets,
|
||||
clearSelect,
|
||||
});
|
||||
</script>
|
||||
|
||||
<ControlAppBar on:close-button-click={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
|
||||
|
|
|
|||
|
|
@ -1,22 +1,71 @@
|
|||
<script lang="ts">
|
||||
import { UserResponseDto, api } from '@api';
|
||||
import { PartnerResponseDto, UserResponseDto, api } from '@api';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import PartnerSelectionModal from './partner-selection-modal.svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { mdiCheck, mdiClose } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
|
||||
|
||||
interface PartnerSharing {
|
||||
user: UserResponseDto;
|
||||
sharedByMe: boolean;
|
||||
sharedWithMe: boolean;
|
||||
inTimeline: boolean;
|
||||
}
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
export let partners: UserResponseDto[];
|
||||
let createPartner = false;
|
||||
let removePartner: UserResponseDto | null = null;
|
||||
let removePartner: PartnerResponseDto | null = null;
|
||||
let partners: Array<PartnerSharing> = [];
|
||||
|
||||
onMount(() => {
|
||||
refreshPartners();
|
||||
});
|
||||
|
||||
const refreshPartners = async () => {
|
||||
const { data } = await api.partnerApi.getPartners({ direction: 'shared-by' });
|
||||
partners = data;
|
||||
partners = [];
|
||||
|
||||
const [{ data: sharedBy }, { data: sharedWith }] = await Promise.all([
|
||||
api.partnerApi.getPartners({ direction: 'shared-by' }),
|
||||
api.partnerApi.getPartners({ direction: 'shared-with' }),
|
||||
]);
|
||||
|
||||
for (const candidate of sharedBy) {
|
||||
partners = [
|
||||
...partners,
|
||||
{
|
||||
user: candidate,
|
||||
sharedByMe: true,
|
||||
sharedWithMe: false,
|
||||
inTimeline: candidate.inTimeline ?? false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
for (const candidate of sharedWith) {
|
||||
const existIndex = partners.findIndex((p) => candidate.id === p.user.id);
|
||||
|
||||
if (existIndex >= 0) {
|
||||
partners[existIndex].sharedWithMe = true;
|
||||
partners[existIndex].inTimeline = candidate.inTimeline ?? false;
|
||||
} else {
|
||||
partners = [
|
||||
...partners,
|
||||
{
|
||||
user: candidate,
|
||||
sharedByMe: false,
|
||||
sharedWithMe: true,
|
||||
inTimeline: candidate.inTimeline ?? false,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePartner = async () => {
|
||||
|
|
@ -45,34 +94,80 @@
|
|||
handleError(error, 'Unable to add partners');
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowOnTimelineChanged = async (partner: PartnerSharing, inTimeline: boolean) => {
|
||||
try {
|
||||
await api.partnerApi.updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } });
|
||||
|
||||
partner.inTimeline = inTimeline;
|
||||
partners = partners;
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to update timeline display status');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
{#if partners.length > 0}
|
||||
<div class="flex flex-row gap-4">
|
||||
{#each partners as partner (partner.id)}
|
||||
<div class="flex gap-4 rounded-lg px-5 py-4 transition-all">
|
||||
<UserAvatar user={partner} size="md" autoColor />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.firstName}
|
||||
{partner.lastName}
|
||||
</p>
|
||||
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
|
||||
{partner.email}
|
||||
</p>
|
||||
{#each partners as partner (partner.user.id)}
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 mt-6 bg-slate-50 dark:bg-gray-900 p-5">
|
||||
<div class="flex gap-4 rounded-lg pb-4 transition-all justify-between">
|
||||
<div class="flex gap-4">
|
||||
<UserAvatar user={partner.user} size="md" autoColor />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.user.firstName}
|
||||
{partner.user.lastName}
|
||||
</p>
|
||||
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
|
||||
{partner.user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CircleIconButton
|
||||
on:click={() => (removePartner = partner)}
|
||||
icon={mdiClose}
|
||||
size={'16'}
|
||||
title="Remove partner"
|
||||
/>
|
||||
|
||||
{#if partner.sharedByMe}
|
||||
<CircleIconButton
|
||||
on:click={() => (removePartner = partner.user)}
|
||||
icon={mdiClose}
|
||||
size={'16'}
|
||||
title="Stop sharing your photos with this user"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="dark:text-gray-200 text-immich-dark-gray">
|
||||
<!-- I am sharing my assets with this user -->
|
||||
{#if partner.sharedByMe}
|
||||
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
|
||||
<p class="text-xs font-medium my-4">SHARED WITH {partner.user.firstName.toUpperCase()}</p>
|
||||
<p class="text-md">{partner.user.firstName} can access</p>
|
||||
<ul class="text-sm">
|
||||
<li class="flex gap-2 place-items-center py-1 mt-2">
|
||||
<Icon path={mdiCheck} /> All your photos and videos except those in Archived and Deleted
|
||||
</li>
|
||||
<li class="flex gap-2 place-items-center py-1">
|
||||
<Icon path={mdiCheck} /> The location where your photos were taken
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<!-- this user is sharing assets with me -->
|
||||
{#if partner.sharedWithMe}
|
||||
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
|
||||
<p class="text-xs font-medium my-4">PHOTOS FROM {partner.user.firstName.toUpperCase()}</p>
|
||||
<SettingSwitch
|
||||
title="Show in timeline"
|
||||
subtitle="Show photos and videos from this user in your timeline"
|
||||
bind:checked={partner.inTimeline}
|
||||
on:toggle={({ detail }) => handleShowOnTimelineChanged(partner, detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="flex justify-end">
|
||||
|
||||
<div class="flex justify-end mt-5">
|
||||
<Button size="sm" on:click={() => (createPartner = true)}>Add partner</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
export let keys: APIKeyResponseDto[] = [];
|
||||
export let devices: AuthDeviceResponseDto[] = [];
|
||||
export let partners: UserResponseDto[] = [];
|
||||
|
||||
let oauthOpen = false;
|
||||
if (browser) {
|
||||
|
|
@ -61,7 +60,7 @@
|
|||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Sharing" subtitle="Manage sharing with partners">
|
||||
<PartnerSettings {user} bind:partners />
|
||||
<PartnerSettings {user} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Sidebar" subtitle="Manage sidebar settings">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue