asset-viewer.svelte 6.0 KB

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