asset-grid.svelte 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. <script lang="ts">
  2. import {
  3. assetInteractionStore,
  4. isViewingAssetStoreState,
  5. viewingAssetStoreState
  6. } from '$lib/stores/asset-interaction.store';
  7. import { assetGridState, assetStore, loadingBucketState } from '$lib/stores/assets.store';
  8. import type { UserResponseDto } from '@api';
  9. import { AssetCountByTimeBucketResponseDto, AssetResponseDto, TimeGroupEnum, api } from '@api';
  10. import { onDestroy, onMount } from 'svelte';
  11. import AssetViewer from '../asset-viewer/asset-viewer.svelte';
  12. import IntersectionObserver from '../asset-viewer/intersection-observer.svelte';
  13. import Portal from '../shared-components/portal/portal.svelte';
  14. import Scrollbar, {
  15. OnScrollbarClickDetail,
  16. OnScrollbarDragDetail
  17. } from '../shared-components/scrollbar/scrollbar.svelte';
  18. import AssetDateGroup from './asset-date-group.svelte';
  19. export let user: UserResponseDto | undefined = undefined;
  20. export let isAlbumSelectionMode = false;
  21. let viewportHeight = 0;
  22. let viewportWidth = 0;
  23. let assetGridElement: HTMLElement;
  24. let bucketInfo: AssetCountByTimeBucketResponseDto;
  25. onMount(async () => {
  26. const { data: assetCountByTimebucket } = await api.assetApi.getAssetCountByTimeBucket({
  27. getAssetCountByTimeBucketDto: {
  28. timeGroup: TimeGroupEnum.Month,
  29. userId: user?.id
  30. }
  31. });
  32. bucketInfo = assetCountByTimebucket;
  33. assetStore.setInitialState(viewportHeight, viewportWidth, assetCountByTimebucket, user?.id);
  34. // Get asset bucket if bucket height is smaller than viewport height
  35. let bucketsToFetchInitially: string[] = [];
  36. let initialBucketsHeight = 0;
  37. $assetGridState.buckets.every((bucket) => {
  38. if (initialBucketsHeight < viewportHeight) {
  39. initialBucketsHeight += bucket.bucketHeight;
  40. bucketsToFetchInitially.push(bucket.bucketDate);
  41. return true;
  42. } else {
  43. return false;
  44. }
  45. });
  46. bucketsToFetchInitially.forEach((bucketDate) => {
  47. assetStore.getAssetsByBucket(bucketDate);
  48. });
  49. });
  50. onDestroy(() => {
  51. assetStore.setInitialState(0, 0, { totalCount: 0, buckets: [] }, undefined);
  52. });
  53. function intersectedHandler(event: CustomEvent) {
  54. const el = event.detail as HTMLElement;
  55. const target = el.firstChild as HTMLElement;
  56. if (target) {
  57. const bucketDate = target.id.split('_')[1];
  58. assetStore.getAssetsByBucket(bucketDate);
  59. }
  60. }
  61. const navigateToPreviousAsset = () => {
  62. assetInteractionStore.navigateAsset('previous');
  63. };
  64. const navigateToNextAsset = () => {
  65. assetInteractionStore.navigateAsset('next');
  66. };
  67. let lastScrollPosition = 0;
  68. let animationTick = false;
  69. const handleTimelineScroll = () => {
  70. if (!animationTick) {
  71. window.requestAnimationFrame(() => {
  72. lastScrollPosition = assetGridElement?.scrollTop;
  73. animationTick = false;
  74. });
  75. animationTick = true;
  76. }
  77. };
  78. const handleScrollbarClick = (e: OnScrollbarClickDetail) => {
  79. assetGridElement.scrollTop = e.scrollTo;
  80. };
  81. const handleScrollbarDrag = (e: OnScrollbarDragDetail) => {
  82. assetGridElement.scrollTop = e.scrollTo;
  83. };
  84. const handleArchiveSuccess = (e: CustomEvent) => {
  85. const asset = e.detail as AssetResponseDto;
  86. navigateToNextAsset();
  87. assetStore.removeAsset(asset.id);
  88. };
  89. </script>
  90. {#if bucketInfo && viewportHeight && $assetGridState.timelineHeight > viewportHeight}
  91. <Scrollbar
  92. scrollbarHeight={viewportHeight}
  93. scrollTop={lastScrollPosition}
  94. on:onscrollbarclick={(e) => handleScrollbarClick(e.detail)}
  95. on:onscrollbardrag={(e) => handleScrollbarDrag(e.detail)}
  96. />
  97. {/if}
  98. <section
  99. id="asset-grid"
  100. class="overflow-y-auto pl-4 scrollbar-hidden"
  101. bind:clientHeight={viewportHeight}
  102. bind:clientWidth={viewportWidth}
  103. bind:this={assetGridElement}
  104. on:scroll={handleTimelineScroll}
  105. >
  106. {#if assetGridElement}
  107. <section id="virtual-timeline" style:height={$assetGridState.timelineHeight + 'px'}>
  108. {#each $assetGridState.buckets as bucket, bucketIndex (bucketIndex)}
  109. <IntersectionObserver
  110. on:intersected={intersectedHandler}
  111. on:hidden={async () => {
  112. // If bucket is hidden and in loading state, cancel the request
  113. if ($loadingBucketState[bucket.bucketDate]) {
  114. await assetStore.cancelBucketRequest(bucket.cancelToken, bucket.bucketDate);
  115. }
  116. }}
  117. let:intersecting
  118. top={750}
  119. bottom={750}
  120. root={assetGridElement}
  121. >
  122. <div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
  123. {#if intersecting}
  124. <AssetDateGroup
  125. {isAlbumSelectionMode}
  126. assets={bucket.assets}
  127. bucketDate={bucket.bucketDate}
  128. bucketHeight={bucket.bucketHeight}
  129. />
  130. {/if}
  131. </div>
  132. </IntersectionObserver>
  133. {/each}
  134. </section>
  135. {/if}
  136. </section>
  137. <Portal target="body">
  138. {#if $isViewingAssetStoreState}
  139. <AssetViewer
  140. asset={$viewingAssetStoreState}
  141. on:navigate-previous={navigateToPreviousAsset}
  142. on:navigate-next={navigateToNextAsset}
  143. on:close={() => {
  144. assetInteractionStore.setIsViewingAsset(false);
  145. }}
  146. on:archived={handleArchiveSuccess}
  147. />
  148. {/if}
  149. </Portal>
  150. <style>
  151. #asset-grid {
  152. contain: layout;
  153. scrollbar-width: none;
  154. }
  155. </style>