scrollbar.svelte 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. <script lang="ts">
  2. import type { AssetStore } from '$lib/stores/assets.store';
  3. import { fromLocalDateTime } from '$lib/utils/timeline-util';
  4. import { createEventDispatcher } from 'svelte';
  5. export let timelineY = 0;
  6. export let height = 0;
  7. export let assetStore: AssetStore;
  8. let isHover = false;
  9. let isDragging = false;
  10. let isAnimating = false;
  11. let hoverLabel = '';
  12. let clientY = 0;
  13. let windowHeight = 0;
  14. const toScrollY = (timelineY: number) => (timelineY / $assetStore.timelineHeight) * height;
  15. const toTimelineY = (scrollY: number) => Math.round((scrollY * $assetStore.timelineHeight) / height);
  16. const HOVER_DATE_HEIGHT = 30;
  17. $: hoverY = height - windowHeight + clientY;
  18. $: scrollY = toScrollY(timelineY);
  19. $: segments = $assetStore.buckets.map((bucket) => ({
  20. count: bucket.assets.length,
  21. height: toScrollY(bucket.bucketHeight),
  22. timeGroup: bucket.bucketDate,
  23. }));
  24. const dispatch = createEventDispatcher<{ scrollTimeline: number }>();
  25. const scrollTimeline = () => dispatch('scrollTimeline', toTimelineY(hoverY));
  26. const handleMouseEvent = (event: { clientY: number; isDragging?: boolean }) => {
  27. const wasDragging = isDragging;
  28. isDragging = event.isDragging ?? isDragging;
  29. clientY = event.clientY;
  30. if (wasDragging === false && isDragging) {
  31. scrollTimeline();
  32. }
  33. if (!isDragging || isAnimating) {
  34. return;
  35. }
  36. isAnimating = true;
  37. window.requestAnimationFrame(() => {
  38. scrollTimeline();
  39. isAnimating = false;
  40. });
  41. };
  42. </script>
  43. <svelte:window bind:innerHeight={windowHeight} />
  44. <!-- svelte-ignore a11y-no-static-element-interactions -->
  45. {#if $assetStore.timelineHeight > height}
  46. <div
  47. id="immich-scrubbable-scrollbar"
  48. class="fixed right-0 z-[1] select-none bg-immich-bg hover:cursor-row-resize"
  49. style:width={isDragging ? '100vw' : '60px'}
  50. style:height={height + 'px'}
  51. style:background-color={isDragging ? 'transparent' : 'transparent'}
  52. draggable="false"
  53. on:mouseenter={() => (isHover = true)}
  54. on:mouseleave={() => {
  55. isHover = false;
  56. isDragging = false;
  57. }}
  58. on:mouseenter={({ clientY, buttons }) => handleMouseEvent({ clientY, isDragging: !!buttons })}
  59. on:mousemove={({ clientY }) => handleMouseEvent({ clientY })}
  60. on:mousedown={({ clientY }) => handleMouseEvent({ clientY, isDragging: true })}
  61. on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
  62. >
  63. {#if isHover}
  64. <div
  65. class="pointer-events-none absolute right-0 z-[100] w-[100px] rounded-tl-md border-b-2 border-immich-primary bg-immich-bg py-1 pl-1 pr-6 text-sm font-medium shadow-lg dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg"
  66. style:top="{Math.max(hoverY - HOVER_DATE_HEIGHT, 0)}px"
  67. >
  68. {hoverLabel}
  69. </div>
  70. {/if}
  71. <!-- Scroll Position Indicator Line -->
  72. {#if !isDragging}
  73. <div
  74. class="absolute right-0 h-[2px] w-10 bg-immich-primary dark:bg-immich-dark-primary"
  75. style:top="{scrollY}px"
  76. />
  77. {/if}
  78. <!-- Time Segment -->
  79. {#each segments as segment, index (segment.timeGroup)}
  80. {@const date = fromLocalDateTime(segment.timeGroup)}
  81. {@const year = date.year}
  82. {@const label = `${date.toLocaleString({ month: 'short' })} ${year}`}
  83. <!-- svelte-ignore a11y-no-static-element-interactions -->
  84. <div
  85. id="time-segment"
  86. class="relative"
  87. style:height={segment.height + 'px'}
  88. aria-label={segment.timeGroup + ' ' + segment.count}
  89. on:mousemove={() => (hoverLabel = label)}
  90. >
  91. {#if new Date(segments[index - 1]?.timeGroup).getFullYear() !== year}
  92. {#if segment.height > 8}
  93. <div
  94. aria-label={segment.timeGroup + ' ' + segment.count}
  95. class="absolute right-0 z-10 pr-5 text-xs font-medium dark:text-immich-dark-fg"
  96. >
  97. {year}
  98. </div>
  99. {/if}
  100. {:else if segment.height > 5}
  101. <div
  102. aria-label={segment.timeGroup + ' ' + segment.count}
  103. class="absolute right-0 mr-3 block h-[4px] w-[4px] rounded-full bg-gray-300"
  104. />
  105. {/if}
  106. </div>
  107. {/each}
  108. </div>
  109. {/if}
  110. <style>
  111. #immich-scrubbable-scrollbar,
  112. #time-segment {
  113. contain: layout;
  114. }
  115. </style>