mirror of
https://github.com/immich-app/immich.git
synced 2025-01-16 00:36:47 +01:00
feat(web): allow ctrl-click / cmd-click on photos (#9954)
* feat(web): allow ctrl-click / cmd-click on photos * fix: photo opening when deselected bug * fix: consistent naming * remove redundant code * fix: disabled picture is clickable in "add to album" grid * remove unnecessary code * cleanup * fix file permissions * fix: album selection bug * fix: stack slideshow bug & search gallery viewer bug * cleanup * fix dark mode stack slideshow bug * cleanup --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
d8d64ecc45
commit
4d862525bc
5 changed files with 28 additions and 14 deletions
|
@ -740,6 +740,7 @@
|
|||
readonly
|
||||
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
|
||||
showStackedIcon={false}
|
||||
isStackSlideshow={true}
|
||||
/>
|
||||
|
||||
{#if stackedAsset.id == asset.id}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
import ImageThumbnail from './image-thumbnail.svelte';
|
||||
import VideoThumbnail from './video-thumbnail.svelte';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: { asset: AssetResponseDto };
|
||||
|
@ -36,6 +37,8 @@
|
|||
export let thumbnailHeight: number | undefined = undefined;
|
||||
export let selected = false;
|
||||
export let selectionCandidate = false;
|
||||
export let isMultiSelectState = false;
|
||||
export let isStackSlideshow = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let showArchiveIcon = false;
|
||||
|
@ -46,7 +49,6 @@
|
|||
export { className as class };
|
||||
|
||||
let mouseOver = false;
|
||||
$: clickable = !disabled && onClick;
|
||||
|
||||
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
|
||||
|
||||
|
@ -62,19 +64,30 @@
|
|||
return [235, 235];
|
||||
})();
|
||||
|
||||
const thumbnailClickedHandler = () => {
|
||||
if (clickable) {
|
||||
const thumbnailClickedHandler = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!disabled) {
|
||||
onClick?.(asset);
|
||||
}
|
||||
};
|
||||
|
||||
const onIconClickedHandler = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!disabled) {
|
||||
dispatch('select', { asset });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
if (isMultiSelectState) {
|
||||
onIconClickedHandler(e as MouseEvent);
|
||||
} else if (isStackSlideshow) {
|
||||
thumbnailClickedHandler(e);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
mouseOver = true;
|
||||
};
|
||||
|
@ -85,24 +98,22 @@
|
|||
</script>
|
||||
|
||||
<IntersectionObserver once={false} on:intersected let:intersecting>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div
|
||||
<a
|
||||
href={currentUrlReplaceAssetId(asset.id)}
|
||||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
class="group focus-visible:outline-none relative overflow-hidden {disabled
|
||||
class="group focus-visible:outline-none flex overflow-hidden {disabled
|
||||
? 'bg-gray-300'
|
||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:hover:cursor-pointer={clickable}
|
||||
on:mouseenter={onMouseEnter}
|
||||
on:mouseleave={onMouseLeave}
|
||||
role={clickable ? 'button' : undefined}
|
||||
tabindex={clickable ? 0 : undefined}
|
||||
on:click={thumbnailClickedHandler}
|
||||
tabindex={0}
|
||||
on:click={handleClick}
|
||||
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: thumbnailClickedHandler }}
|
||||
>
|
||||
{#if intersecting}
|
||||
<div class="absolute z-20 h-full w-full {className}">
|
||||
<div class="absolute z-20 {className}" style:width="{width}px" style:height="{height}px">
|
||||
<!-- Select asset button -->
|
||||
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
||||
<button
|
||||
|
@ -128,7 +139,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="absolute h-full w-full select-none bg-gray-100 transition-transform dark:bg-immich-dark-gray"
|
||||
class="absolute h-full w-full select-none bg-transparent transition-transform"
|
||||
class:scale-[0.85]={selected}
|
||||
class:rounded-xl={selected}
|
||||
>
|
||||
|
@ -227,5 +238,5 @@
|
|||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
</IntersectionObserver>
|
||||
|
|
|
@ -179,6 +179,7 @@
|
|||
on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)}
|
||||
selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
|
||||
selectionCandidate={$assetSelectionCandidates.has(asset)}
|
||||
isMultiSelectState={$isMultiSelectState || isSelectionMode}
|
||||
disabled={$assetStore.albumAssets.has(asset.id)}
|
||||
thumbnailWidth={box.width}
|
||||
thumbnailHeight={box.height}
|
||||
|
|
|
@ -128,6 +128,7 @@
|
|||
on:intersected={(event) =>
|
||||
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||
selected={selectedAssets.has(asset)}
|
||||
isMultiSelectState={isMultiSelectionMode}
|
||||
{showArchiveIcon}
|
||||
thumbnailWidth={geometry.boxes[i].width}
|
||||
thumbnailHeight={geometry.boxes[i].height}
|
||||
|
|
|
@ -31,7 +31,7 @@ function currentUrlWithoutAsset() {
|
|||
: $page.url.pathname.replace(/(\/photos.*)$/, '') + $page.url.search;
|
||||
}
|
||||
|
||||
function currentUrlReplaceAssetId(assetId: string) {
|
||||
export function currentUrlReplaceAssetId(assetId: string) {
|
||||
const $page = get(page);
|
||||
// this contains special casing for the /photos/:assetId photos route, which hangs directly
|
||||
// off / instead of a subpath, unlike every other asset-containing route.
|
||||
|
|
Loading…
Reference in a new issue