asset-viewer.svelte 5.6 KB

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