mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(web): improve shared link management on mobile (#11720)
* feat(web): improve shared link management on mobile * fix format
This commit is contained in:
		
							parent
							
								
									9837d60074
								
							
						
					
					
						commit
						276101ee82
					
				@ -19,7 +19,7 @@ describe('AlbumCover component', () => {
 | 
				
			|||||||
    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
					    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
				
			||||||
    expect(img.alt).toBe('someName');
 | 
					    expect(img.alt).toBe('someName');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('lazy');
 | 
					    expect(img.getAttribute('loading')).toBe('lazy');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover text');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text');
 | 
				
			||||||
    expect(img.getAttribute('src')).toBe('/asdf');
 | 
					    expect(img.getAttribute('src')).toBe('/asdf');
 | 
				
			||||||
    expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' });
 | 
					    expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -36,7 +36,7 @@ describe('AlbumCover component', () => {
 | 
				
			|||||||
    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
					    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
				
			||||||
    expect(img.alt).toBe('unnamed_album');
 | 
					    expect(img.alt).toBe('unnamed_album');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('eager');
 | 
					    expect(img.getAttribute('loading')).toBe('eager');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover asdf');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf');
 | 
				
			||||||
    expect(img.getAttribute('src')).toStrictEqual(expect.any(String));
 | 
					    expect(img.getAttribute('src')).toStrictEqual(expect.any(String));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -14,10 +14,8 @@
 | 
				
			|||||||
  $: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
 | 
					  $: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="relative aspect-square">
 | 
					{#if thumbnailUrl}
 | 
				
			||||||
  {#if thumbnailUrl}
 | 
					  <AssetCover {alt} class={className} src={thumbnailUrl} {preload} />
 | 
				
			||||||
    <AssetCover {alt} class={className} src={thumbnailUrl} {preload} />
 | 
					{:else}
 | 
				
			||||||
  {:else}
 | 
					  <NoCover {alt} class={className} {preload} />
 | 
				
			||||||
    <NoCover {alt} class={className} {preload} />
 | 
					{/if}
 | 
				
			||||||
  {/if}
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -32,6 +32,7 @@
 | 
				
			|||||||
   * Additional classes to apply to the button.
 | 
					   * Additional classes to apply to the button.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  export let buttonClass: string | undefined = undefined;
 | 
					  export let buttonClass: string | undefined = undefined;
 | 
				
			||||||
 | 
					  export let hideContent = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let isOpen = false;
 | 
					  let isOpen = false;
 | 
				
			||||||
  let contextMenuPosition = { x: 0, y: 0 };
 | 
					  let contextMenuPosition = { x: 0, y: 0 };
 | 
				
			||||||
@ -125,30 +126,32 @@
 | 
				
			|||||||
      on:click={handleClick}
 | 
					      on:click={handleClick}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div
 | 
					  {#if isOpen || !hideContent}
 | 
				
			||||||
    use:shortcuts={[
 | 
					    <div
 | 
				
			||||||
      {
 | 
					      use:shortcuts={[
 | 
				
			||||||
        shortcut: { key: 'Tab' },
 | 
					        {
 | 
				
			||||||
        onShortcut: closeDropdown,
 | 
					          shortcut: { key: 'Tab' },
 | 
				
			||||||
        preventDefault: false,
 | 
					          onShortcut: closeDropdown,
 | 
				
			||||||
      },
 | 
					          preventDefault: false,
 | 
				
			||||||
      {
 | 
					        },
 | 
				
			||||||
        shortcut: { key: 'Tab', shift: true },
 | 
					        {
 | 
				
			||||||
        onShortcut: closeDropdown,
 | 
					          shortcut: { key: 'Tab', shift: true },
 | 
				
			||||||
        preventDefault: false,
 | 
					          onShortcut: closeDropdown,
 | 
				
			||||||
      },
 | 
					          preventDefault: false,
 | 
				
			||||||
    ]}
 | 
					        },
 | 
				
			||||||
  >
 | 
					      ]}
 | 
				
			||||||
    <ContextMenu
 | 
					 | 
				
			||||||
      {...contextMenuPosition}
 | 
					 | 
				
			||||||
      {direction}
 | 
					 | 
				
			||||||
      ariaActiveDescendant={$selectedIdStore}
 | 
					 | 
				
			||||||
      ariaLabelledBy={buttonId}
 | 
					 | 
				
			||||||
      bind:menuElement={menuContainer}
 | 
					 | 
				
			||||||
      id={menuId}
 | 
					 | 
				
			||||||
      isVisible={isOpen}
 | 
					 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <slot />
 | 
					      <ContextMenu
 | 
				
			||||||
    </ContextMenu>
 | 
					        {...contextMenuPosition}
 | 
				
			||||||
  </div>
 | 
					        {direction}
 | 
				
			||||||
 | 
					        ariaActiveDescendant={$selectedIdStore}
 | 
				
			||||||
 | 
					        ariaLabelledBy={buttonId}
 | 
				
			||||||
 | 
					        bind:menuElement={menuContainer}
 | 
				
			||||||
 | 
					        id={menuId}
 | 
				
			||||||
 | 
					        isVisible={isOpen}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <slot />
 | 
				
			||||||
 | 
					      </ContextMenu>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
 | 
					  import { serverConfig } from '$lib/stores/server-config.store';
 | 
				
			||||||
 | 
					  import { copyToClipboard, makeSharedLinkUrl } from '$lib/utils';
 | 
				
			||||||
 | 
					  import type { SharedLinkResponseDto } from '@immich/sdk';
 | 
				
			||||||
 | 
					  import { mdiContentCopy } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export let link: SharedLinkResponseDto;
 | 
				
			||||||
 | 
					  export let menuItem = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCopy = async () => {
 | 
				
			||||||
 | 
					    await copyToClipboard(makeSharedLinkUrl($serverConfig.externalDomain, link.key));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if menuItem}
 | 
				
			||||||
 | 
					  <MenuOption text={$t('copy_link')} icon={mdiContentCopy} onClick={handleCopy} />
 | 
				
			||||||
 | 
					{:else}
 | 
				
			||||||
 | 
					  <CircleIconButton title={$t('copy_link')} icon={mdiContentCopy} on:click={handleCopy} />
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
 | 
					  import { mdiDelete } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export let menuItem = false;
 | 
				
			||||||
 | 
					  export let onDelete: () => void;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if menuItem}
 | 
				
			||||||
 | 
					  <MenuOption text={$t('delete_link')} icon={mdiDelete} onClick={onDelete} />
 | 
				
			||||||
 | 
					{:else}
 | 
				
			||||||
 | 
					  <CircleIconButton title={$t('delete_link')} icon={mdiDelete} on:click={onDelete} />
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
 | 
				
			||||||
 | 
					  import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
 | 
					  import { mdiCircleEditOutline } from '@mdi/js';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  export let menuItem = false;
 | 
				
			||||||
 | 
					  export let onEdit: () => void;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if menuItem}
 | 
				
			||||||
 | 
					  <MenuOption text={$t('edit_link')} icon={mdiCircleEditOutline} onClick={onEdit} />
 | 
				
			||||||
 | 
					{:else}
 | 
				
			||||||
 | 
					  <CircleIconButton title={$t('edit_link')} icon={mdiCircleEditOutline} on:click={onEdit} />
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
@ -13,6 +13,6 @@ describe('AssetCover component', () => {
 | 
				
			|||||||
    expect(img.alt).toBe('123');
 | 
					    expect(img.alt).toBe('123');
 | 
				
			||||||
    expect(img.getAttribute('src')).toBe('wee');
 | 
					    expect(img.getAttribute('src')).toBe('wee');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('eager');
 | 
					    expect(img.getAttribute('loading')).toBe('eager');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover asdf');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ describe('NoCover component', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
					    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
				
			||||||
    expect(img.alt).toBe('123');
 | 
					    expect(img.alt).toBe('123');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover asdf');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('eager');
 | 
					    expect(img.getAttribute('loading')).toBe('eager');
 | 
				
			||||||
    expect(img.src).toStrictEqual(expect.any(String));
 | 
					    expect(img.src).toStrictEqual(expect.any(String));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ describe('ShareCover component', () => {
 | 
				
			|||||||
    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
					    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
				
			||||||
    expect(img.alt).toBe('123');
 | 
					    expect(img.alt).toBe('123');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('lazy');
 | 
					    expect(img.getAttribute('loading')).toBe('lazy');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover text');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('renders an image when the shared link is an individual share', () => {
 | 
					  it('renders an image when the shared link is an individual share', () => {
 | 
				
			||||||
@ -30,7 +30,7 @@ describe('ShareCover component', () => {
 | 
				
			|||||||
    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
					    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
				
			||||||
    expect(img.alt).toBe('individual_share');
 | 
					    expect(img.alt).toBe('individual_share');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('lazy');
 | 
					    expect(img.getAttribute('loading')).toBe('lazy');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover text');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text');
 | 
				
			||||||
    expect(img.getAttribute('src')).toBe('/asdf');
 | 
					    expect(img.getAttribute('src')).toBe('/asdf');
 | 
				
			||||||
    expect(getAssetThumbnailUrl).toHaveBeenCalledWith('someId');
 | 
					    expect(getAssetThumbnailUrl).toHaveBeenCalledWith('someId');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -44,7 +44,7 @@ describe('ShareCover component', () => {
 | 
				
			|||||||
    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
					    const img = component.getByTestId('album-image') as HTMLImageElement;
 | 
				
			||||||
    expect(img.alt).toBe('unnamed_share');
 | 
					    expect(img.alt).toBe('unnamed_share');
 | 
				
			||||||
    expect(img.getAttribute('loading')).toBe('lazy');
 | 
					    expect(img.getAttribute('loading')).toBe('lazy');
 | 
				
			||||||
    expect(img.className).toBe('z-0 rounded-xl object-cover text');
 | 
					    expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('renders fallback image when asset is not resized', () => {
 | 
					  it('renders fallback image when asset is not resized', () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<img
 | 
					<img
 | 
				
			||||||
  {alt}
 | 
					  {alt}
 | 
				
			||||||
  class="z-0 rounded-xl object-cover {className}"
 | 
					  class="z-0 rounded-xl object-cover aspect-square {className}"
 | 
				
			||||||
  data-testid="album-image"
 | 
					  data-testid="album-image"
 | 
				
			||||||
  draggable="false"
 | 
					  draggable="false"
 | 
				
			||||||
  loading={preload ? 'eager' : 'lazy'}
 | 
					  loading={preload ? 'eager' : 'lazy'}
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<enhanced:img
 | 
					<enhanced:img
 | 
				
			||||||
  {alt}
 | 
					  {alt}
 | 
				
			||||||
  class="z-0 rounded-xl object-cover {className}"
 | 
					  class="z-0 rounded-xl object-cover aspect-square {className}"
 | 
				
			||||||
  data-testid="album-image"
 | 
					  data-testid="album-image"
 | 
				
			||||||
  draggable="false"
 | 
					  draggable="false"
 | 
				
			||||||
  loading={preload ? 'eager' : 'lazy'}
 | 
					  loading={preload ? 'eager' : 'lazy'}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@
 | 
				
			|||||||
  export { className as class };
 | 
					  export { className as class };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="relative aspect-square shrink-0">
 | 
					<div class="relative shrink-0">
 | 
				
			||||||
  {#if link?.album}
 | 
					  {#if link?.album}
 | 
				
			||||||
    <AlbumCover album={link.album} class={className} {preload} />
 | 
					    <AlbumCover album={link.album} class={className} {preload} />
 | 
				
			||||||
  {:else if link.assets[0]?.resized}
 | 
					  {:else if link.assets[0]?.resized}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +1,20 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import Badge from '$lib/components/elements/badge.svelte';
 | 
					  import Badge from '$lib/components/elements/badge.svelte';
 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					 | 
				
			||||||
  import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
 | 
					  import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
 | 
				
			||||||
  import { AppRoute } from '$lib/constants';
 | 
					  import { AppRoute } from '$lib/constants';
 | 
				
			||||||
  import { locale } from '$lib/stores/preferences.store';
 | 
					  import { locale } from '$lib/stores/preferences.store';
 | 
				
			||||||
  import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
 | 
					  import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { DateTime, type ToRelativeUnit } from 'luxon';
 | 
					  import { DateTime, type ToRelativeUnit } from 'luxon';
 | 
				
			||||||
  import { createEventDispatcher } from 'svelte';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
 | 
					  import SharedLinkDelete from '$lib/components/sharedlinks-page/actions/shared-link-delete.svelte';
 | 
				
			||||||
 | 
					  import SharedLinkEdit from '$lib/components/sharedlinks-page/actions/shared-link-edit.svelte';
 | 
				
			||||||
 | 
					  import SharedLinkCopy from '$lib/components/sharedlinks-page/actions/shared-link-copy.svelte';
 | 
				
			||||||
 | 
					  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 | 
				
			||||||
 | 
					  import { mdiDotsVertical } from '@mdi/js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let link: SharedLinkResponseDto;
 | 
					  export let link: SharedLinkResponseDto;
 | 
				
			||||||
 | 
					  export let onDelete: () => void;
 | 
				
			||||||
  const dispatch = createEventDispatcher<{
 | 
					  export let onEdit: () => void;
 | 
				
			||||||
    delete: void;
 | 
					 | 
				
			||||||
    copy: void;
 | 
					 | 
				
			||||||
    edit: void;
 | 
					 | 
				
			||||||
  }>();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let now = DateTime.now();
 | 
					  let now = DateTime.now();
 | 
				
			||||||
  $: expiresAt = link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined;
 | 
					  $: expiresAt = link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined;
 | 
				
			||||||
@ -37,69 +34,84 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
  class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
 | 
					  class="flex w-full border-b border-gray-200 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
  <ShareCover class="size-24 transition-all duration-300 hover:shadow-lg" {link} />
 | 
					  <svelte:element
 | 
				
			||||||
 | 
					    this={isExpired ? 'div' : 'a'}
 | 
				
			||||||
 | 
					    href={isExpired ? undefined : `${AppRoute.SHARE}/${link.key}`}
 | 
				
			||||||
 | 
					    class="flex gap-4 w-full py-4"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <ShareCover class="size-24 transition-all duration-300 hover:shadow-lg" {link} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-col justify-between">
 | 
					    <div class="flex flex-col justify-between">
 | 
				
			||||||
    <div class="info-top">
 | 
					      <div class="info-top">
 | 
				
			||||||
      <div class="font-mono text-xs font-semibold text-gray-500 dark:text-gray-400">
 | 
					        <div class="font-mono text-xs font-semibold text-gray-500 dark:text-gray-400">
 | 
				
			||||||
        {#if isExpired}
 | 
					          {#if isExpired}
 | 
				
			||||||
          <p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p>
 | 
					            <p class="font-bold text-red-600 dark:text-red-400">{$t('expired')}</p>
 | 
				
			||||||
        {:else if expiresAt}
 | 
					          {:else if expiresAt}
 | 
				
			||||||
          <p>
 | 
					 | 
				
			||||||
            {$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        {:else}
 | 
					 | 
				
			||||||
          <p>{$t('expires_date', { values: { date: '∞' } })}</p>
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="text-sm">
 | 
					 | 
				
			||||||
        <div class="flex place-items-center gap-2 text-immich-primary dark:text-immich-dark-primary">
 | 
					 | 
				
			||||||
          {#if link.type === SharedLinkType.Album}
 | 
					 | 
				
			||||||
            <p>
 | 
					            <p>
 | 
				
			||||||
              {link.album?.albumName.toUpperCase()}
 | 
					              {$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}
 | 
				
			||||||
            </p>
 | 
					            </p>
 | 
				
			||||||
          {:else if link.type === SharedLinkType.Individual}
 | 
					          {:else}
 | 
				
			||||||
            <p>{$t('individual_share').toUpperCase()}</p>
 | 
					            <p>{$t('expires_date', { values: { date: '∞' } })}</p>
 | 
				
			||||||
          {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          {#if !isExpired}
 | 
					 | 
				
			||||||
            <a href="{AppRoute.SHARE}/{link.key}" title={$t('go_to_share_page')}>
 | 
					 | 
				
			||||||
              <Icon path={mdiOpenInNew} />
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
          {/if}
 | 
					          {/if}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <p class="text-sm">{link.description ?? ''}</p>
 | 
					        <div class="text-sm pb-2">
 | 
				
			||||||
 | 
					          <p
 | 
				
			||||||
 | 
					            class="flex place-items-center gap-2 text-immich-primary dark:text-immich-dark-primary break-all uppercase"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {#if link.type === SharedLinkType.Album}
 | 
				
			||||||
 | 
					              {link.album?.albumName}
 | 
				
			||||||
 | 
					            {:else if link.type === SharedLinkType.Individual}
 | 
				
			||||||
 | 
					              {$t('individual_share')}
 | 
				
			||||||
 | 
					            {/if}
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <p class="text-sm">{link.description ?? ''}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="flex flex-wrap gap-2 text-xl">
 | 
				
			||||||
 | 
					        {#if link.allowUpload}
 | 
				
			||||||
 | 
					          <Badge rounded="full"><span class="text-xs px-1">{$t('upload')}</span></Badge>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {#if link.allowDownload}
 | 
				
			||||||
 | 
					          <Badge rounded="full"><span class="text-xs px-1">{$t('download')}</span></Badge>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {#if link.showMetadata}
 | 
				
			||||||
 | 
					          <Badge rounded="full"><span class="text-xs px-1">{$t('exif').toUpperCase()}</span></Badge>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {#if link.password}
 | 
				
			||||||
 | 
					          <Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  </svelte:element>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="info-bottom flex gap-4 text-xl">
 | 
					  <div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
 | 
				
			||||||
      {#if link.allowUpload}
 | 
					    <div class="sm:flex hidden">
 | 
				
			||||||
        <Badge rounded="full"><span class="text-xs px-1">{$t('upload')}</span></Badge>
 | 
					      <SharedLinkEdit {onEdit} />
 | 
				
			||||||
      {/if}
 | 
					      <SharedLinkCopy {link} />
 | 
				
			||||||
 | 
					      <SharedLinkDelete {onDelete} />
 | 
				
			||||||
      {#if link.allowDownload}
 | 
					 | 
				
			||||||
        <Badge rounded="full"><span class="text-xs px-1">{$t('download')}</span></Badge>
 | 
					 | 
				
			||||||
      {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {#if link.showMetadata}
 | 
					 | 
				
			||||||
        <Badge rounded="full"><span class="text-xs px-1">{$t('exif').toUpperCase()}</span></Badge>
 | 
					 | 
				
			||||||
      {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {#if link.password}
 | 
					 | 
				
			||||||
        <Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge>
 | 
					 | 
				
			||||||
      {/if}
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-auto flex-col place-content-center place-items-end text-right">
 | 
					    <div class="sm:hidden">
 | 
				
			||||||
    <div class="flex">
 | 
					      <ButtonContextMenu
 | 
				
			||||||
      <CircleIconButton title={$t('delete_link')} icon={mdiDelete} on:click={() => dispatch('delete')} />
 | 
					        color="transparent"
 | 
				
			||||||
      <CircleIconButton title={$t('edit_link')} icon={mdiCircleEditOutline} on:click={() => dispatch('edit')} />
 | 
					        title={$t('shared_link_options')}
 | 
				
			||||||
      <CircleIconButton title={$t('copy_link')} icon={mdiContentCopy} on:click={() => dispatch('copy')} />
 | 
					        icon={mdiDotsVertical}
 | 
				
			||||||
 | 
					        size="24"
 | 
				
			||||||
 | 
					        padding="3"
 | 
				
			||||||
 | 
					        hideContent
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <SharedLinkEdit menuItem {onEdit} />
 | 
				
			||||||
 | 
					        <SharedLinkCopy menuItem {link} />
 | 
				
			||||||
 | 
					        <SharedLinkDelete menuItem {onDelete} />
 | 
				
			||||||
 | 
					      </ButtonContextMenu>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -696,7 +696,6 @@
 | 
				
			|||||||
  "getting_started": "Getting Started",
 | 
					  "getting_started": "Getting Started",
 | 
				
			||||||
  "go_back": "Go back",
 | 
					  "go_back": "Go back",
 | 
				
			||||||
  "go_to_search": "Go to search",
 | 
					  "go_to_search": "Go to search",
 | 
				
			||||||
  "go_to_share_page": "Go to share page",
 | 
					 | 
				
			||||||
  "group_albums_by": "Group albums by...",
 | 
					  "group_albums_by": "Group albums by...",
 | 
				
			||||||
  "group_no": "No grouping",
 | 
					  "group_no": "No grouping",
 | 
				
			||||||
  "group_owner": "Group by owner",
 | 
					  "group_owner": "Group by owner",
 | 
				
			||||||
@ -1078,6 +1077,7 @@
 | 
				
			|||||||
  "shared_by_user": "Shared by {user}",
 | 
					  "shared_by_user": "Shared by {user}",
 | 
				
			||||||
  "shared_by_you": "Shared by you",
 | 
					  "shared_by_you": "Shared by you",
 | 
				
			||||||
  "shared_from_partner": "Photos from {partner}",
 | 
					  "shared_from_partner": "Photos from {partner}",
 | 
				
			||||||
 | 
					  "shared_link_options": "Shared link options",
 | 
				
			||||||
  "shared_links": "Shared links",
 | 
					  "shared_links": "Shared links",
 | 
				
			||||||
  "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
 | 
					  "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
 | 
				
			||||||
  "shared_with_partner": "Shared with {partner}",
 | 
					  "shared_with_partner": "Shared with {partner}",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
					 | 
				
			||||||
  import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
					  import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
				
			||||||
  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
 | 
					  import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
 | 
				
			||||||
  import {
 | 
					  import {
 | 
				
			||||||
@ -9,8 +8,6 @@
 | 
				
			|||||||
  } from '$lib/components/shared-components/notification/notification';
 | 
					  } from '$lib/components/shared-components/notification/notification';
 | 
				
			||||||
  import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
 | 
					  import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
 | 
				
			||||||
  import { AppRoute } from '$lib/constants';
 | 
					  import { AppRoute } from '$lib/constants';
 | 
				
			||||||
  import { serverConfig } from '$lib/stores/server-config.store';
 | 
					 | 
				
			||||||
  import { copyToClipboard, makeSharedLinkUrl } from '$lib/utils';
 | 
					 | 
				
			||||||
  import { handleError } from '$lib/utils/handle-error';
 | 
					  import { handleError } from '$lib/utils/handle-error';
 | 
				
			||||||
  import { getAllSharedLinks, removeSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
 | 
					  import { getAllSharedLinks, removeSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
 | 
				
			||||||
  import { mdiArrowLeft } from '@mdi/js';
 | 
					  import { mdiArrowLeft } from '@mdi/js';
 | 
				
			||||||
@ -53,35 +50,26 @@
 | 
				
			|||||||
    await refresh();
 | 
					    await refresh();
 | 
				
			||||||
    editSharedLink = null;
 | 
					    editSharedLink = null;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleCopyLink = async (key: string) => {
 | 
					 | 
				
			||||||
    await copyToClipboard(makeSharedLinkUrl($serverConfig.externalDomain, key));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<ControlAppBar backIcon={mdiArrowLeft} on:close={() => goto(AppRoute.SHARING)}>
 | 
					<ControlAppBar backIcon={mdiArrowLeft} on:close={() => goto(AppRoute.SHARING)}>
 | 
				
			||||||
  <svelte:fragment slot="leading">{$t('shared_links')}</svelte:fragment>
 | 
					  <svelte:fragment slot="leading">{$t('shared_links')}</svelte:fragment>
 | 
				
			||||||
</ControlAppBar>
 | 
					</ControlAppBar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section class="mt-[120px] flex flex-col pb-[120px]">
 | 
					<section class="mt-[120px] flex flex-col pb-[120px] container max-w-screen-lg mx-auto px-3">
 | 
				
			||||||
  <div class="m-auto mb-4 w-[50%] dark:text-immich-gray">
 | 
					  <div class="mb-4 dark:text-immich-gray">
 | 
				
			||||||
    <p>{$t('manage_shared_links')}</p>
 | 
					    <p>{$t('manage_shared_links')}</p>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  {#if sharedLinks.length === 0}
 | 
					  {#if sharedLinks.length === 0}
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      class="m-auto flex w-[50%] place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
 | 
					      class="flex place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <p>{$t('you_dont_have_any_shared_links')}</p>
 | 
					      <p>{$t('you_dont_have_any_shared_links')}</p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {:else}
 | 
					  {:else}
 | 
				
			||||||
    <div class="m-auto flex w-[50%] flex-col">
 | 
					    <div class="flex flex-col">
 | 
				
			||||||
      {#each sharedLinks as link (link.id)}
 | 
					      {#each sharedLinks as link (link.id)}
 | 
				
			||||||
        <SharedLinkCard
 | 
					        <SharedLinkCard {link} onDelete={() => handleDeleteLink(link.id)} onEdit={() => (editSharedLink = link)} />
 | 
				
			||||||
          {link}
 | 
					 | 
				
			||||||
          on:delete={() => handleDeleteLink(link.id)}
 | 
					 | 
				
			||||||
          on:edit={() => (editSharedLink = link)}
 | 
					 | 
				
			||||||
          on:copy={() => handleCopyLink(link.key)}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      {/each}
 | 
					      {/each}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user