mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:06:56 -04:00
Merge 6f0a7d0638f8e90c6287d84c5aa1cb02b86854b4 into 4d20b11f256c40e3894c229ed638d7ea04ebdc44
This commit is contained in:
commit
3e50cc5384
@ -43,6 +43,7 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let album: AlbumResponseDto | null = null;
|
export let album: AlbumResponseDto | null = null;
|
||||||
@ -82,6 +83,11 @@
|
|||||||
class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
|
class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white"
|
||||||
data-testid="asset-viewer-navbar-actions"
|
data-testid="asset-viewer-navbar-actions"
|
||||||
>
|
>
|
||||||
|
{#if asset.owner && asset.owner.id != $user.id}
|
||||||
|
<div class="p-3 margin:auto">
|
||||||
|
<UserAvatar user={asset.owner} size="xs"></UserAvatar>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if !asset.isTrashed && $user}
|
{#if !asset.isTrashed && $user}
|
||||||
<ShareAction {asset} />
|
<ShareAction {asset} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -124,7 +130,6 @@
|
|||||||
title={$t('editor')}
|
title={$t('editor')}
|
||||||
/>
|
/>
|
||||||
{/if} -->
|
{/if} -->
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<DeleteAction {asset} {onAction} />
|
<DeleteAction {asset} {onAction} />
|
||||||
|
|
||||||
|
@ -462,9 +462,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
|
{#if (currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner && asset.ownerId != $user.id) || (asset.ownerId != $user.id && asset.owner)}
|
||||||
<section class="px-6 dark:text-immich-dark-fg mt-4">
|
<section class="px-6 dark:text-immich-dark-fg mt-4">
|
||||||
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
|
{#if currentAlbum}
|
||||||
|
<p class="text-sm">{$t('shared_by').toUpperCase()}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm">{$t('partner_sharing').toUpperCase()}</p>
|
||||||
|
{/if}
|
||||||
<div class="flex gap-4 pt-4">
|
<div class="flex gap-4 pt-4">
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar user={asset.owner} size="md" />
|
<UserAvatar user={asset.owner} size="md" />
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
import { intersectionObserver } from '$lib/actions/intersection-observer';
|
import { intersectionObserver } from '$lib/actions/intersection-observer';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { ProjectionType } from '$lib/constants';
|
import { ProjectionType } from '$lib/constants';
|
||||||
import { getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
|
import { getAssetThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { timeToSeconds } from '$lib/utils/date-time';
|
import { timeToSeconds } from '$lib/utils/date-time';
|
||||||
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
|
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||||
|
import { locale, playVideoThumbnailOnHover, showUserThumbnails } from '$lib/stores/preferences.store';
|
||||||
|
import { getUserAndCacheResult } from '$lib/utils/users';
|
||||||
import { getAssetPlaybackUrl } from '$lib/utils';
|
import { getAssetPlaybackUrl } from '$lib/utils';
|
||||||
import {
|
import {
|
||||||
mdiArchiveArrowDownOutline,
|
mdiArchiveArrowDownOutline,
|
||||||
@ -19,6 +22,7 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
import ImageThumbnail from './image-thumbnail.svelte';
|
import ImageThumbnail from './image-thumbnail.svelte';
|
||||||
import VideoThumbnail from './video-thumbnail.svelte';
|
import VideoThumbnail from './video-thumbnail.svelte';
|
||||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||||
@ -30,6 +34,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
import { thumbhash } from '$lib/actions/thumbhash';
|
||||||
|
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let dateGroup: DateGroup | undefined = undefined;
|
export let dateGroup: DateGroup | undefined = undefined;
|
||||||
@ -44,6 +49,7 @@
|
|||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
export let showStackedIcon = true;
|
export let showStackedIcon = true;
|
||||||
|
export let showUserThumbnailsinViewer = true;
|
||||||
export let intersectionConfig: {
|
export let intersectionConfig: {
|
||||||
root?: HTMLElement;
|
root?: HTMLElement;
|
||||||
bottom?: string;
|
bottom?: string;
|
||||||
@ -63,7 +69,6 @@
|
|||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
let {
|
let {
|
||||||
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
|
||||||
} = TUNABLES;
|
} = TUNABLES;
|
||||||
@ -74,6 +79,7 @@
|
|||||||
let intersecting = false;
|
let intersecting = false;
|
||||||
let lastRetrievedElement: HTMLElement | undefined;
|
let lastRetrievedElement: HTMLElement | undefined;
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
let shareUser: UserResponseDto | undefined;
|
||||||
|
|
||||||
$: if (!retrieveElement) {
|
$: if (!retrieveElement) {
|
||||||
lastRetrievedElement = undefined;
|
lastRetrievedElement = undefined;
|
||||||
@ -82,6 +88,9 @@
|
|||||||
lastRetrievedElement = element;
|
lastRetrievedElement = element;
|
||||||
onRetrieveElement?.(element);
|
onRetrieveElement?.(element);
|
||||||
}
|
}
|
||||||
|
$: if ($showUserThumbnails && showUserThumbnailsinViewer && (isSharedLink() || asset.ownerId != $user.id)) {
|
||||||
|
handlePromiseError(getShareUser());
|
||||||
|
}
|
||||||
|
|
||||||
$: width = thumbnailSize || thumbnailWidth || 235;
|
$: width = thumbnailSize || thumbnailWidth || 235;
|
||||||
$: height = thumbnailSize || thumbnailHeight || 235;
|
$: height = thumbnailSize || thumbnailHeight || 235;
|
||||||
@ -159,6 +168,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getShareUser = async () => {
|
||||||
|
try {
|
||||||
|
shareUser = await getUserAndCacheResult(asset.ownerId);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_load_liked_status'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
assetStore?.taskManager.removeAllTasksForComponent(componentId);
|
assetStore?.taskManager.removeAllTasksForComponent(componentId);
|
||||||
});
|
});
|
||||||
@ -268,6 +285,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if shareUser && showUserThumbnailsinViewer}
|
||||||
|
<div class="absolute bottom-2 left-2 z-10">
|
||||||
|
<UserAvatar user={shareUser} size="sm" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
|
{#if !isSharedLink() && showArchiveIcon && asset.isArchived}
|
||||||
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
|
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
|
||||||
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
export let singleSelect = false;
|
export let singleSelect = false;
|
||||||
export let withStacked = false;
|
export let withStacked = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
|
export let showUserThumbnailsinViewer = true;
|
||||||
export let assetGridElement: HTMLElement | undefined = undefined;
|
export let assetGridElement: HTMLElement | undefined = undefined;
|
||||||
export let renderThumbsAtBottomMargin: string | undefined = undefined;
|
export let renderThumbsAtBottomMargin: string | undefined = undefined;
|
||||||
export let renderThumbsAtTopMargin: string | undefined = undefined;
|
export let renderThumbsAtTopMargin: string | undefined = undefined;
|
||||||
@ -207,6 +208,7 @@
|
|||||||
onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)}
|
onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)}
|
||||||
showStackedIcon={withStacked}
|
showStackedIcon={withStacked}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
|
{showUserThumbnailsinViewer}
|
||||||
{asset}
|
{asset}
|
||||||
{groupIndex}
|
{groupIndex}
|
||||||
onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
|
onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
export let withStacked = false;
|
export let withStacked = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
export let isShared = false;
|
export let isShared = false;
|
||||||
|
export let showUserThumbnailsinViewer = true;
|
||||||
export let album: AlbumResponseDto | null = null;
|
export let album: AlbumResponseDto | null = null;
|
||||||
export let isShowDeleteConfirmation = false;
|
export let isShowDeleteConfirmation = false;
|
||||||
export let onSelect: (asset: AssetResponseDto) => void = () => {};
|
export let onSelect: (asset: AssetResponseDto) => void = () => {};
|
||||||
@ -839,6 +840,7 @@
|
|||||||
renderThumbsAtBottomMargin={THUMBNAIL_INTERSECTION_ROOT_BOTTOM}
|
renderThumbsAtBottomMargin={THUMBNAIL_INTERSECTION_ROOT_BOTTOM}
|
||||||
{withStacked}
|
{withStacked}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
|
{showUserThumbnailsinViewer}
|
||||||
{assetStore}
|
{assetStore}
|
||||||
{assetInteractionStore}
|
{assetInteractionStore}
|
||||||
{isSelectionMode}
|
{isSelectionMode}
|
||||||
@ -864,6 +866,7 @@
|
|||||||
<AssetViewer
|
<AssetViewer
|
||||||
{withStacked}
|
{withStacked}
|
||||||
{assetStore}
|
{assetStore}
|
||||||
|
{showUserThumbnailsinViewer}
|
||||||
asset={$viewingAsset}
|
asset={$viewingAsset}
|
||||||
preloadAssets={$preloadAssets}
|
preloadAssets={$preloadAssets}
|
||||||
{isShared}
|
{isShared}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
|
export type Size = 'full' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -56,6 +56,7 @@
|
|||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
full: 'w-full h-full',
|
full: 'w-full h-full',
|
||||||
|
xs: 'w-6 h-6',
|
||||||
sm: 'w-7 h-7',
|
sm: 'w-7 h-7',
|
||||||
md: 'w-10 h-10',
|
md: 'w-10 h-10',
|
||||||
lg: 'w-12 h-12',
|
lg: 'w-12 h-12',
|
||||||
@ -90,7 +91,8 @@
|
|||||||
{#if showFallback}
|
{#if showFallback}
|
||||||
<span
|
<span
|
||||||
class="flex h-full w-full select-none items-center justify-center font-medium"
|
class="flex h-full w-full select-none items-center justify-center font-medium"
|
||||||
class:text-xs={size === 'sm'}
|
class:text-xs={size === 'xs'}
|
||||||
|
class:text-sm={size === 'sm'}
|
||||||
class:text-lg={size === 'lg'}
|
class:text-lg={size === 'lg'}
|
||||||
class:text-xl={size === 'xl'}
|
class:text-xl={size === 'xl'}
|
||||||
class:text-2xl={size === 'xxl'}
|
class:text-2xl={size === 'xxl'}
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
loopVideo,
|
loopVideo,
|
||||||
playVideoThumbnailOnHover,
|
playVideoThumbnailOnHover,
|
||||||
showDeleteModal,
|
showDeleteModal,
|
||||||
|
showUserThumbnails,
|
||||||
} from '$lib/stores/preferences.store';
|
} from '$lib/stores/preferences.store';
|
||||||
import { findLocale } from '$lib/utils';
|
import { findLocale } from '$lib/utils';
|
||||||
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
|
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
|
||||||
@ -169,6 +170,14 @@
|
|||||||
bind:checked={$showDeleteModal}
|
bind:checked={$showDeleteModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-4">
|
||||||
|
<SettingSwitch
|
||||||
|
title={$t('show_user_thumbnails')}
|
||||||
|
subtitle={$t('show_user_thumbnails_description')}
|
||||||
|
bind:checked={$showUserThumbnails}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1153,6 +1153,8 @@
|
|||||||
"show_slideshow_transition": "Show slideshow transition",
|
"show_slideshow_transition": "Show slideshow transition",
|
||||||
"show_supporter_badge": "Supporter badge",
|
"show_supporter_badge": "Supporter badge",
|
||||||
"show_supporter_badge_description": "Show a supporter badge",
|
"show_supporter_badge_description": "Show a supporter badge",
|
||||||
|
"show_user_thumbnails": "Show user thumbnails",
|
||||||
|
"show_user_thumbnails_description": "Show user avatars on timelinle for shared albums and partners",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "Shuffle",
|
||||||
"sidebar": "Sidebar",
|
"sidebar": "Sidebar",
|
||||||
"sidebar_display_description": "Display a link to the view in the sidebar",
|
"sidebar_display_description": "Display a link to the view in the sidebar",
|
||||||
|
@ -139,6 +139,8 @@ export const albumViewSettings = persisted<AlbumViewSettings>('album-view-settin
|
|||||||
|
|
||||||
export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true, {});
|
export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true, {});
|
||||||
|
|
||||||
|
export const showUserThumbnails = persisted<boolean>('show-user-thumbnails', true, {});
|
||||||
|
|
||||||
export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {});
|
export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {});
|
||||||
|
|
||||||
export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {});
|
export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {});
|
||||||
|
35
web/src/lib/stores/users.store.ts
Normal file
35
web/src/lib/stores/users.store.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { type UserResponseDto } from '@immich/sdk';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const users = writable<{ [key: string]: UserResponseDto | undefined }>({});
|
||||||
|
|
||||||
|
export const userExistsInStore = (userId: string): boolean => {
|
||||||
|
let exists = false;
|
||||||
|
users.subscribe((userStore) => {
|
||||||
|
try {
|
||||||
|
exists = userId in userStore;
|
||||||
|
} catch {
|
||||||
|
exists = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return exists;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserInStore = ({ user, userId }: { user?: UserResponseDto; userId?: string }) => {
|
||||||
|
users.update((userStore) => {
|
||||||
|
if (user) {
|
||||||
|
userStore[user.id] = user;
|
||||||
|
} else if (userId) {
|
||||||
|
userStore[userId] = undefined;
|
||||||
|
}
|
||||||
|
return userStore;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserFromStore = (userId: string): UserResponseDto | undefined => {
|
||||||
|
let userInfo: UserResponseDto | undefined;
|
||||||
|
users.subscribe((userStore) => {
|
||||||
|
userInfo = userStore[userId];
|
||||||
|
})();
|
||||||
|
return userInfo;
|
||||||
|
};
|
16
web/src/lib/utils/users.ts
Normal file
16
web/src/lib/utils/users.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { getUserFromStore, updateUserInStore, userExistsInStore } from '$lib/stores/users.store';
|
||||||
|
import { getUser, type UserResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
|
export const getUserAndCacheResult = async (userId: string, skipCache: boolean = false): Promise<UserResponseDto> => {
|
||||||
|
let user: UserResponseDto;
|
||||||
|
if (!skipCache && userExistsInStore(userId)) {
|
||||||
|
user = getUserFromStore(userId)!;
|
||||||
|
} else {
|
||||||
|
//Add to store indicating a request to server is in-flight
|
||||||
|
updateUserInStore({ userId });
|
||||||
|
user = await getUser({ id: userId });
|
||||||
|
//Update store with results of server request
|
||||||
|
updateUserInStore({ user });
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
@ -46,5 +46,5 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
{/if}
|
{/if}
|
||||||
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} />
|
<AssetGrid enableRouting={true} {assetStore} {assetInteractionStore} showUserThumbnailsinViewer={false} />
|
||||||
</main>
|
</main>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user