shared-link-card.svelte 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <script lang="ts">
  2. import { api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType, ThumbnailFormat } from '@api';
  3. import LoadingSpinner from '../shared-components/loading-spinner.svelte';
  4. import Icon from '$lib/components/elements/icon.svelte';
  5. import * as luxon from 'luxon';
  6. import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
  7. import { createEventDispatcher } from 'svelte';
  8. import { goto } from '$app/navigation';
  9. import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
  10. import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
  11. export let link: SharedLinkResponseDto;
  12. let expirationCountdown: luxon.DurationObjectUnits;
  13. const dispatch = createEventDispatcher();
  14. const getThumbnail = async (): Promise<AssetResponseDto> => {
  15. let assetId = '';
  16. if (link.album?.albumThumbnailAssetId) {
  17. assetId = link.album.albumThumbnailAssetId;
  18. } else if (link.assets.length > 0) {
  19. assetId = link.assets[0].id;
  20. }
  21. const { data } = await api.assetApi.getAssetById({ id: assetId });
  22. return data;
  23. };
  24. const getCountDownExpirationDate = () => {
  25. if (!link.expiresAt) {
  26. return;
  27. }
  28. const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString());
  29. const now = luxon.DateTime.now();
  30. expirationCountdown = expiresAtDate.diff(now, ['days', 'hours', 'minutes', 'seconds']).toObject();
  31. if (expirationCountdown.days && expirationCountdown.days > 0) {
  32. return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
  33. } else if (expirationCountdown.hours && expirationCountdown.hours > 0) {
  34. return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'hours' });
  35. } else if (expirationCountdown.minutes && expirationCountdown.minutes > 0) {
  36. return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'minutes' });
  37. } else if (expirationCountdown.seconds && expirationCountdown.seconds > 0) {
  38. return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'seconds' });
  39. }
  40. };
  41. const isExpired = (expiresAt: string) => {
  42. const now = new Date().getTime();
  43. const expiration = new Date(expiresAt).getTime();
  44. return now > expiration;
  45. };
  46. </script>
  47. <div
  48. 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"
  49. >
  50. <div>
  51. {#if link?.album?.albumThumbnailAssetId || link.assets.length > 0}
  52. {#await getThumbnail()}
  53. <LoadingSpinner />
  54. {:then asset}
  55. <img
  56. id={asset.id}
  57. src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
  58. alt={asset.id}
  59. class="h-[100px] w-[100px] rounded-lg object-cover"
  60. loading="lazy"
  61. draggable="false"
  62. />
  63. {/await}
  64. {:else}
  65. <img
  66. src={noThumbnailUrl}
  67. alt={'Album without assets'}
  68. class="h-[100px] w-[100px] rounded-lg object-cover"
  69. loading="lazy"
  70. draggable="false"
  71. />
  72. {/if}
  73. </div>
  74. <div class="flex flex-col justify-between">
  75. <div class="info-top">
  76. <div class="font-mono text-xs font-semibold text-gray-500 dark:text-gray-400">
  77. {#if link.expiresAt}
  78. {#if isExpired(link.expiresAt)}
  79. <p class="font-bold text-red-600 dark:text-red-400">Expired</p>
  80. {:else}
  81. <p>
  82. Expires {getCountDownExpirationDate()}
  83. </p>
  84. {/if}
  85. {:else}
  86. <p>Expires ∞</p>
  87. {/if}
  88. </div>
  89. <div class="text-sm">
  90. <div class="flex place-items-center gap-2 text-immich-primary dark:text-immich-dark-primary">
  91. {#if link.type === SharedLinkType.Album}
  92. <p>
  93. {link.album?.albumName.toUpperCase()}
  94. </p>
  95. {:else if link.type === SharedLinkType.Individual}
  96. <p>INDIVIDUAL SHARE</p>
  97. {/if}
  98. {#if !link.expiresAt || !isExpired(link.expiresAt)}
  99. <!-- svelte-ignore a11y-no-static-element-interactions -->
  100. <div
  101. class="hover:cursor-pointer"
  102. title="Go to share page"
  103. on:click={() => goto(`/share/${link.key}`)}
  104. on:keydown={() => goto(`/share/${link.key}`)}
  105. >
  106. <Icon path={mdiOpenInNew} />
  107. </div>
  108. {/if}
  109. </div>
  110. <p class="text-sm">{link.description ?? ''}</p>
  111. </div>
  112. </div>
  113. <div class="info-bottom flex gap-4">
  114. {#if link.allowUpload}
  115. <div
  116. class="flex w-[80px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
  117. >
  118. Upload
  119. </div>
  120. {/if}
  121. {#if link.allowDownload}
  122. <div
  123. class="flex w-[100px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
  124. >
  125. Download
  126. </div>
  127. {/if}
  128. {#if link.showMetadata}
  129. <div
  130. class="flex w-[60px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
  131. >
  132. EXIF
  133. </div>
  134. {/if}
  135. {#if link.password}
  136. <div
  137. class="flex w-[100px] place-content-center place-items-center rounded-full bg-immich-primary px-2 py-1 text-xs text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray"
  138. >
  139. Password
  140. </div>
  141. {/if}
  142. </div>
  143. </div>
  144. <div class="flex flex-auto flex-col place-content-center place-items-end text-right">
  145. <div class="flex">
  146. <CircleIconButton icon={mdiDelete} on:click={() => dispatch('delete')} />
  147. <CircleIconButton icon={mdiCircleEditOutline} on:click={() => dispatch('edit')} />
  148. <CircleIconButton icon={mdiContentCopy} on:click={() => dispatch('copy')} />
  149. </div>
  150. </div>
  151. </div>