asset-viewer.svelte 5.3 KB

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