mirror of
https://github.com/immich-app/immich.git
synced 2025-01-28 06:32:44 +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
web/src/lib
components
asset-viewer
assets/thumbnail
photos-page
shared-components/gallery-viewer
utils
|
@ -740,6 +740,7 @@
|
||||||
readonly
|
readonly
|
||||||
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
|
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
|
||||||
showStackedIcon={false}
|
showStackedIcon={false}
|
||||||
|
isStackSlideshow={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if stackedAsset.id == asset.id}
|
{#if stackedAsset.id == asset.id}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
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 { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
|
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
select: { asset: AssetResponseDto };
|
select: { asset: AssetResponseDto };
|
||||||
|
@ -36,6 +37,8 @@
|
||||||
export let thumbnailHeight: number | undefined = undefined;
|
export let thumbnailHeight: number | undefined = undefined;
|
||||||
export let selected = false;
|
export let selected = false;
|
||||||
export let selectionCandidate = false;
|
export let selectionCandidate = false;
|
||||||
|
export let isMultiSelectState = false;
|
||||||
|
export let isStackSlideshow = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
|
@ -46,7 +49,6 @@
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
let mouseOver = false;
|
let mouseOver = false;
|
||||||
$: clickable = !disabled && onClick;
|
|
||||||
|
|
||||||
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
|
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
|
||||||
|
|
||||||
|
@ -62,19 +64,30 @@
|
||||||
return [235, 235];
|
return [235, 235];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const thumbnailClickedHandler = () => {
|
const thumbnailClickedHandler = (e: Event) => {
|
||||||
if (clickable) {
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (!disabled) {
|
||||||
onClick?.(asset);
|
onClick?.(asset);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onIconClickedHandler = (e: MouseEvent) => {
|
const onIconClickedHandler = (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
dispatch('select', { asset });
|
dispatch('select', { asset });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: Event) => {
|
||||||
|
if (isMultiSelectState) {
|
||||||
|
onIconClickedHandler(e as MouseEvent);
|
||||||
|
} else if (isStackSlideshow) {
|
||||||
|
thumbnailClickedHandler(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onMouseEnter = () => {
|
const onMouseEnter = () => {
|
||||||
mouseOver = true;
|
mouseOver = true;
|
||||||
};
|
};
|
||||||
|
@ -85,24 +98,22 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IntersectionObserver once={false} on:intersected let:intersecting>
|
<IntersectionObserver once={false} on:intersected let:intersecting>
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<a
|
||||||
<div
|
href={currentUrlReplaceAssetId(asset.id)}
|
||||||
style:width="{width}px"
|
style:width="{width}px"
|
||||||
style:height="{height}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-gray-300'
|
||||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||||
class:cursor-not-allowed={disabled}
|
class:cursor-not-allowed={disabled}
|
||||||
class:hover:cursor-pointer={clickable}
|
|
||||||
on:mouseenter={onMouseEnter}
|
on:mouseenter={onMouseEnter}
|
||||||
on:mouseleave={onMouseLeave}
|
on:mouseleave={onMouseLeave}
|
||||||
role={clickable ? 'button' : undefined}
|
tabindex={0}
|
||||||
tabindex={clickable ? 0 : undefined}
|
on:click={handleClick}
|
||||||
on:click={thumbnailClickedHandler}
|
|
||||||
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: thumbnailClickedHandler }}
|
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: thumbnailClickedHandler }}
|
||||||
>
|
>
|
||||||
{#if intersecting}
|
{#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 -->
|
<!-- Select asset button -->
|
||||||
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
||||||
<button
|
<button
|
||||||
|
@ -128,7 +139,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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:scale-[0.85]={selected}
|
||||||
class:rounded-xl={selected}
|
class:rounded-xl={selected}
|
||||||
>
|
>
|
||||||
|
@ -227,5 +238,5 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</a>
|
||||||
</IntersectionObserver>
|
</IntersectionObserver>
|
||||||
|
|
|
@ -179,6 +179,7 @@
|
||||||
on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)}
|
on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)}
|
||||||
selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
|
selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
|
||||||
selectionCandidate={$assetSelectionCandidates.has(asset)}
|
selectionCandidate={$assetSelectionCandidates.has(asset)}
|
||||||
|
isMultiSelectState={$isMultiSelectState || isSelectionMode}
|
||||||
disabled={$assetStore.albumAssets.has(asset.id)}
|
disabled={$assetStore.albumAssets.has(asset.id)}
|
||||||
thumbnailWidth={box.width}
|
thumbnailWidth={box.width}
|
||||||
thumbnailHeight={box.height}
|
thumbnailHeight={box.height}
|
||||||
|
|
|
@ -128,6 +128,7 @@
|
||||||
on:intersected={(event) =>
|
on:intersected={(event) =>
|
||||||
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||||
selected={selectedAssets.has(asset)}
|
selected={selectedAssets.has(asset)}
|
||||||
|
isMultiSelectState={isMultiSelectionMode}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
thumbnailWidth={geometry.boxes[i].width}
|
thumbnailWidth={geometry.boxes[i].width}
|
||||||
thumbnailHeight={geometry.boxes[i].height}
|
thumbnailHeight={geometry.boxes[i].height}
|
||||||
|
|
|
@ -31,7 +31,7 @@ function currentUrlWithoutAsset() {
|
||||||
: $page.url.pathname.replace(/(\/photos.*)$/, '') + $page.url.search;
|
: $page.url.pathname.replace(/(\/photos.*)$/, '') + $page.url.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentUrlReplaceAssetId(assetId: string) {
|
export function currentUrlReplaceAssetId(assetId: string) {
|
||||||
const $page = get(page);
|
const $page = get(page);
|
||||||
// this contains special casing for the /photos/:assetId photos route, which hangs directly
|
// this contains special casing for the /photos/:assetId photos route, which hangs directly
|
||||||
// off / instead of a subpath, unlike every other asset-containing route.
|
// off / instead of a subpath, unlike every other asset-containing route.
|
||||||
|
|
Loading…
Reference in a new issue