mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(web): navigate with keyboard on person page (#5486)
* feat: navigate with keyboard on person page * pr feedback * pr feedback * pr feedback * fix: remove unused import
This commit is contained in:
		
							parent
							
								
									b4579e788b
								
							
						
					
					
						commit
						dcfd1f9ea6
					
				@ -27,6 +27,7 @@
 | 
				
			|||||||
  export let fullwidth = false;
 | 
					  export let fullwidth = false;
 | 
				
			||||||
  export let border = false;
 | 
					  export let border = false;
 | 
				
			||||||
  export let title: string | undefined = '';
 | 
					  export let title: string | undefined = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let className = '';
 | 
					  let className = '';
 | 
				
			||||||
  export { className as class };
 | 
					  export { className as class };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,12 +8,6 @@
 | 
				
			|||||||
  import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js';
 | 
					  import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js';
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const dispatch = createEventDispatcher<{
 | 
					 | 
				
			||||||
    reject: void;
 | 
					 | 
				
			||||||
    confirm: [PersonResponseDto, PersonResponseDto];
 | 
					 | 
				
			||||||
    close: void;
 | 
					 | 
				
			||||||
  }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  export let personMerge1: PersonResponseDto;
 | 
					  export let personMerge1: PersonResponseDto;
 | 
				
			||||||
  export let personMerge2: PersonResponseDto;
 | 
					  export let personMerge2: PersonResponseDto;
 | 
				
			||||||
  export let potentialMergePeople: PersonResponseDto[];
 | 
					  export let potentialMergePeople: PersonResponseDto[];
 | 
				
			||||||
@ -22,6 +16,21 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const title = personMerge2.name;
 | 
					  const title = personMerge2.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dispatch = createEventDispatcher<{
 | 
				
			||||||
 | 
					    reject: void;
 | 
				
			||||||
 | 
					    confirm: [PersonResponseDto, PersonResponseDto];
 | 
				
			||||||
 | 
					    close: void;
 | 
				
			||||||
 | 
					  }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyboardPress = (event: KeyboardEvent) => {
 | 
				
			||||||
 | 
					    switch (event.key) {
 | 
				
			||||||
 | 
					      case 'Escape': {
 | 
				
			||||||
 | 
					        dispatch('close');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const changePersonToMerge = (newperson: PersonResponseDto) => {
 | 
					  const changePersonToMerge = (newperson: PersonResponseDto) => {
 | 
				
			||||||
    const index = potentialMergePeople.indexOf(newperson);
 | 
					    const index = potentialMergePeople.indexOf(newperson);
 | 
				
			||||||
    [potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]];
 | 
					    [potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]];
 | 
				
			||||||
@ -29,6 +38,8 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:document on:keypress={handleKeyboardPress} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<FullScreenModal on:clickOutside={() => dispatch('close')}>
 | 
					<FullScreenModal on:clickOutside={() => dispatch('close')}>
 | 
				
			||||||
  <div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
 | 
					  <div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
@ -114,7 +125,7 @@
 | 
				
			|||||||
        <p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
 | 
					        <p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="mt-8 flex w-full gap-4 px-4 pb-4">
 | 
					      <div class="mt-8 flex w-full gap-4 px-4 pb-4">
 | 
				
			||||||
        <Button color="gray" fullwidth on:click={() => dispatch('reject')}>No</Button>
 | 
					        <Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
 | 
				
			||||||
        <Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
 | 
					        <Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -88,6 +88,8 @@
 | 
				
			|||||||
   **/
 | 
					   **/
 | 
				
			||||||
  let searchWord: string;
 | 
					  let searchWord: string;
 | 
				
			||||||
  let isSearchingPeople = false;
 | 
					  let isSearchingPeople = false;
 | 
				
			||||||
 | 
					  let focusedElements: (HTMLButtonElement | null)[] = Array.from({ length: maximumLengthSearchPeople }, () => null);
 | 
				
			||||||
 | 
					  let indexFocus: number | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const searchPeople = async () => {
 | 
					  const searchPeople = async () => {
 | 
				
			||||||
    if ((people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) || name === '') {
 | 
					    if ((people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) || name === '') {
 | 
				
			||||||
@ -116,6 +118,7 @@
 | 
				
			|||||||
  $: {
 | 
					  $: {
 | 
				
			||||||
    if (people) {
 | 
					    if (people) {
 | 
				
			||||||
      suggestedPeople = name ? searchNameLocal(name, people, 5, data.person.id) : [];
 | 
					      suggestedPeople = name ? searchNameLocal(name, people, 5, data.person.id) : [];
 | 
				
			||||||
 | 
					      indexFocus = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -129,8 +132,51 @@
 | 
				
			|||||||
      viewMode = ViewMode.MERGE_PEOPLE;
 | 
					      viewMode = ViewMode.MERGE_PEOPLE;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyboardPress = (event: KeyboardEvent) => {
 | 
				
			||||||
 | 
					    if (suggestedPeople.length === 0) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!$showAssetViewer) {
 | 
				
			||||||
 | 
					      event.stopPropagation();
 | 
				
			||||||
 | 
					      switch (event.key) {
 | 
				
			||||||
 | 
					        case 'ArrowDown': {
 | 
				
			||||||
 | 
					          event.preventDefault();
 | 
				
			||||||
 | 
					          if (indexFocus === null) {
 | 
				
			||||||
 | 
					            indexFocus = 0;
 | 
				
			||||||
 | 
					          } else if (indexFocus === suggestedPeople.length - 1) {
 | 
				
			||||||
 | 
					            indexFocus = 0;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            indexFocus++;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          focusedElements[indexFocus]?.focus();
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 'ArrowUp': {
 | 
				
			||||||
 | 
					          if (indexFocus === null) {
 | 
				
			||||||
 | 
					            indexFocus = 0;
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (indexFocus === 0) {
 | 
				
			||||||
 | 
					            indexFocus = suggestedPeople.length - 1;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            indexFocus--;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          focusedElements[indexFocus]?.focus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 'Enter': {
 | 
				
			||||||
 | 
					          if (indexFocus !== null) {
 | 
				
			||||||
 | 
					            handleSuggestPeople(suggestedPeople[indexFocus]);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleEscape = () => {
 | 
					  const handleEscape = () => {
 | 
				
			||||||
    if ($showAssetViewer) {
 | 
					    if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if ($isMultiSelectState) {
 | 
					    if ($isMultiSelectState) {
 | 
				
			||||||
@ -350,6 +396,7 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:document on:keydown={handleKeyboardPress} />
 | 
				
			||||||
{#if viewMode === ViewMode.UNASSIGN_ASSETS}
 | 
					{#if viewMode === ViewMode.UNASSIGN_ASSETS}
 | 
				
			||||||
  <UnMergeFaceSelector
 | 
					  <UnMergeFaceSelector
 | 
				
			||||||
    assetIds={[...$selectedAssets].map((a) => a.id)}
 | 
					    assetIds={[...$selectedAssets].map((a) => a.id)}
 | 
				
			||||||
@ -499,13 +546,14 @@
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              {:else}
 | 
					              {:else}
 | 
				
			||||||
                {#each suggestedPeople as person, index (person.id)}
 | 
					                {#each suggestedPeople as person, index (person.id)}
 | 
				
			||||||
                  <div
 | 
					                  <button
 | 
				
			||||||
                    class="flex border-t border-x border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] {index ===
 | 
					                    bind:this={focusedElements[index]}
 | 
				
			||||||
 | 
					                    class="flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
 | 
				
			||||||
                    suggestedPeople.length - 1
 | 
					                    suggestedPeople.length - 1
 | 
				
			||||||
                      ? 'rounded-b-lg border-b'
 | 
					                      ? 'rounded-b-lg border-b'
 | 
				
			||||||
                      : ''}"
 | 
					                      : ''}"
 | 
				
			||||||
 | 
					                    on:click={() => handleSuggestPeople(person)}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <button class="flex w-full place-items-center" on:click={() => handleSuggestPeople(person)}>
 | 
					 | 
				
			||||||
                    <ImageThumbnail
 | 
					                    <ImageThumbnail
 | 
				
			||||||
                      circle
 | 
					                      circle
 | 
				
			||||||
                      shadow
 | 
					                      shadow
 | 
				
			||||||
@ -516,7 +564,6 @@
 | 
				
			|||||||
                    />
 | 
					                    />
 | 
				
			||||||
                    <p class="ml-4 text-gray-700 dark:text-gray-100">{person.name}</p>
 | 
					                    <p class="ml-4 text-gray-700 dark:text-gray-100">{person.name}</p>
 | 
				
			||||||
                  </button>
 | 
					                  </button>
 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                {/each}
 | 
					                {/each}
 | 
				
			||||||
              {/if}
 | 
					              {/if}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user