From 4d862525bcb5f00f8160e3308a9c09b60bb2a746 Mon Sep 17 00:00:00 2001
From: George Shao <GeorgeShao123@gmail.com>
Date: Sat, 8 Jun 2024 16:33:23 -0400
Subject: [PATCH] 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>
---
 .../asset-viewer/asset-viewer.svelte          |  1 +
 .../assets/thumbnail/thumbnail.svelte         | 37 ++++++++++++-------
 .../photos-page/asset-date-group.svelte       |  1 +
 .../gallery-viewer/gallery-viewer.svelte      |  1 +
 web/src/lib/utils/navigation.ts               |  2 +-
 5 files changed, 28 insertions(+), 14 deletions(-)

diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 3518528986..a7e4b87195 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -740,6 +740,7 @@
                 readonly
                 thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
                 showStackedIcon={false}
+                isStackSlideshow={true}
               />
 
               {#if stackedAsset.id == asset.id}
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
index bd2dbd6fff..5c86801160 100644
--- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
@@ -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>
diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte
index c6b9998d85..d7aefa24ec 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -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}
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
index 8afebc50db..27bd088a80 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
@@ -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}
diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts
index 356ea888a4..4d5660f173 100644
--- a/web/src/lib/utils/navigation.ts
+++ b/web/src/lib/utils/navigation.ts
@@ -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.