asset-viewer.svelte 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <script lang="ts">
  2. import { createEventDispatcher, onDestroy, onMount } from 'svelte';
  3. import { fly } from 'svelte/transition';
  4. import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
  5. import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
  6. import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
  7. import PhotoViewer from './photo-viewer.svelte';
  8. import DetailPanel from './detail-panel.svelte';
  9. import { session } from '$app/stores';
  10. import { downloadAssets } from '$lib/stores/download';
  11. import VideoViewer from './video-viewer.svelte';
  12. import { api, AssetResponseDto, AssetTypeEnum } from '@api';
  13. import { browser } from '$app/env';
  14. const dispatch = createEventDispatcher();
  15. export let asset: AssetResponseDto;
  16. let halfLeftHover = false;
  17. let halfRightHover = false;
  18. let isShowDetail = false;
  19. onMount(() => {
  20. if (browser) {
  21. document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
  22. }
  23. });
  24. const handleKeyboardPress = (key: string) => {
  25. switch (key) {
  26. case 'Escape':
  27. closeViewer();
  28. return;
  29. case 'i':
  30. isShowDetail = !isShowDetail;
  31. return;
  32. case 'ArrowLeft':
  33. navigateAssetBackward();
  34. return;
  35. case 'ArrowRight':
  36. navigateAssetForward();
  37. return;
  38. }
  39. };
  40. const closeViewer = () => {
  41. dispatch('close');
  42. };
  43. const navigateAssetForward = (e?: Event) => {
  44. e?.stopPropagation();
  45. dispatch('navigate-forward');
  46. };
  47. const navigateAssetBackward = (e?: Event) => {
  48. e?.stopPropagation();
  49. dispatch('navigate-backward');
  50. };
  51. const showDetailInfoHandler = () => {
  52. isShowDetail = !isShowDetail;
  53. };
  54. const downloadFile = async () => {
  55. if ($session.user) {
  56. try {
  57. const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
  58. const imageExtension = asset.originalPath.split('.')[1];
  59. const imageFileName = imageName + '.' + imageExtension;
  60. // If assets is already download -> return;
  61. if ($downloadAssets[imageFileName]) {
  62. return;
  63. }
  64. $downloadAssets[imageFileName] = 0;
  65. const { data, status } = await api.assetApi.downloadFile(
  66. asset.deviceAssetId,
  67. asset.deviceId,
  68. false,
  69. false,
  70. {
  71. responseType: 'blob',
  72. onDownloadProgress: (progressEvent) => {
  73. if (progressEvent.lengthComputable) {
  74. const total = progressEvent.total;
  75. const current = progressEvent.loaded;
  76. let percentCompleted = Math.floor((current / total) * 100);
  77. $downloadAssets[imageFileName] = percentCompleted;
  78. }
  79. }
  80. }
  81. );
  82. if (!(data instanceof Blob)) {
  83. return;
  84. }
  85. if (status === 200) {
  86. const fileUrl = URL.createObjectURL(data);
  87. const anchor = document.createElement('a');
  88. anchor.href = fileUrl;
  89. anchor.download = imageFileName;
  90. document.body.appendChild(anchor);
  91. anchor.click();
  92. document.body.removeChild(anchor);
  93. URL.revokeObjectURL(fileUrl);
  94. // Remove item from download list
  95. setTimeout(() => {
  96. const copy = $downloadAssets;
  97. delete copy[imageFileName];
  98. $downloadAssets = copy;
  99. }, 2000);
  100. }
  101. } catch (e) {
  102. console.log('Error downloading file ', e);
  103. }
  104. }
  105. };
  106. </script>
  107. <section
  108. id="immich-asset-viewer"
  109. class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 "
  110. >
  111. <div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
  112. <AsserViewerNavBar
  113. on:goBack={closeViewer}
  114. on:showDetail={showDetailInfoHandler}
  115. on:download={downloadFile}
  116. />
  117. </div>
  118. <div
  119. class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${
  120. asset.type == AssetTypeEnum.Video ? '' : 'z-[999]'
  121. }`}
  122. on:mouseenter={() => {
  123. halfLeftHover = true;
  124. halfRightHover = false;
  125. }}
  126. on:mouseleave={() => {
  127. halfLeftHover = false;
  128. }}
  129. on:click={navigateAssetBackward}
  130. >
  131. <button
  132. class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
  133. class:navigation-button-hover={halfLeftHover}
  134. on:click={navigateAssetBackward}
  135. >
  136. <ChevronLeft size="36" />
  137. </button>
  138. </div>
  139. <div class="row-start-1 row-span-full col-start-1 col-span-4">
  140. {#key asset.id}
  141. {#if asset.type == AssetTypeEnum.Image}
  142. <PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer} />
  143. {:else}
  144. <VideoViewer assetId={asset.id} on:close={closeViewer} />
  145. {/if}
  146. {/key}
  147. </div>
  148. <div
  149. class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${
  150. asset.type == AssetTypeEnum.Video ? '' : 'z-[500]'
  151. }`}
  152. on:click={navigateAssetForward}
  153. on:mouseenter={() => {
  154. halfLeftHover = false;
  155. halfRightHover = true;
  156. }}
  157. on:mouseleave={() => {
  158. halfRightHover = false;
  159. }}
  160. >
  161. <button
  162. class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]"
  163. class:navigation-button-hover={halfRightHover}
  164. on:click={navigateAssetForward}
  165. >
  166. <ChevronRight size="36" />
  167. </button>
  168. </div>
  169. {#if isShowDetail}
  170. <div
  171. transition:fly={{ duration: 150 }}
  172. id="detail-panel"
  173. class="bg-immich-bg w-[360px] row-span-full transition-all "
  174. translate="yes"
  175. >
  176. <DetailPanel {asset} on:close={() => (isShowDetail = false)} />
  177. </div>
  178. {/if}
  179. </section>
  180. <style>
  181. .navigation-button-hover {
  182. background-color: rgb(107 114 128 / var(--tw-bg-opacity));
  183. color: rgb(55 65 81 / var(--tw-text-opacity));
  184. transition: all 150ms;
  185. }
  186. </style>