itube-specs 0.0.849 → 0.0.850

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,10 +11,9 @@
11
11
  <NuxtLink
12
12
  :to="link"
13
13
  class="card-video__poster-wrapper"
14
- @mouseenter.passive="playVideo"
15
- @touchmove.passive="playVideo"
16
- @mouseleave.passive="stopVideo"
17
- @touchend.passive="stopVideo"
14
+ @mouseenter.passive="startPreviewByHover(previewId)"
15
+ @mouseleave.passive="stopPreviewByHover(previewId)"
16
+ @touchstart.passive="startPreviewByTouch(previewId)"
18
17
  >
19
18
  <UiImg
20
19
  :src="currentPoster"
@@ -163,17 +162,12 @@ const { generateLink } = useGenerateLink();
163
162
  const pageName = useGetPureRouteName();
164
163
  const isChannelPage = computed(() => pageName === 'channels-channel');
165
164
 
166
- const isPreviewShow = ref(false);
165
+ const previewId = useId();
166
+ const { activePreviewId, startPreviewByHover, stopPreviewByHover, startPreviewByTouch } = useVideoPreview();
167
167
 
168
- const dropdownMounted = ref(false);
169
-
170
- function playVideo() {
171
- isPreviewShow.value = true;
172
- }
168
+ const isPreviewShow = computed(() => activePreviewId.value === previewId);
173
169
 
174
- function stopVideo() {
175
- isPreviewShow.value = false;
176
- }
170
+ const dropdownMounted = ref(false);
177
171
 
178
172
  const posterError = ref(false);
179
173
  const avatarError = ref(false);
@@ -1,28 +1,103 @@
1
- <!-- Видео-предпросмотр (автоплей) поверх постера карточки видео -->
1
+ <!-- Видео-предпросмотр (автоплей) поверх постера карточки видео + индикатор загрузки -->
2
2
  <template>
3
- <video
4
- :src="url"
5
- class="card-video-preview"
6
- loop
7
- muted
8
- autoplay
9
- playsinline
10
- />
3
+ <span class="card-video-preview">
4
+ <video
5
+ ref="videoRef"
6
+ :src="url"
7
+ :class="{'--playing': isPlaying}"
8
+ class="card-video-preview__video"
9
+ loop
10
+ muted
11
+ autoplay
12
+ playsinline
13
+ @loadedmetadata="updateProgress"
14
+ @progress="updateProgress"
15
+ @playing="onPlaying"
16
+ @waiting="onWaiting"
17
+ @pause="isPlaying = false"
18
+ />
19
+ <span
20
+ :class="{'--done': isLoaded}"
21
+ :style="{'--preview-progress': isLoaded ? 1 : progress}"
22
+ class="card-video-preview__loader"
23
+ />
24
+ </span>
11
25
  </template>
12
26
 
13
27
  <script setup lang="ts">
14
28
  defineProps<{
15
29
  url: string
16
30
  }>();
31
+
32
+ const videoRef = ref<HTMLVideoElement | null>(null);
33
+ const isPlaying = ref(false);
34
+ const isLoaded = ref(false);
35
+ const progress = ref(0);
36
+
37
+ /** Доля скачанного файла превью: правый край последнего буферизованного отрезка к общей длительности. */
38
+ function updateProgress() {
39
+ const video = videoRef.value;
40
+ if (!video || !video.buffered.length || !Number.isFinite(video.duration)) return;
41
+
42
+ progress.value = video.buffered.end(video.buffered.length - 1) / video.duration;
43
+ if (progress.value >= 1) isLoaded.value = true;
44
+ }
45
+
46
+ function onPlaying() {
47
+ isPlaying.value = true;
48
+ isLoaded.value = true;
49
+ }
50
+
51
+ /** Буферизация на середине: возвращаем постер и полосу, она продолжит с реально скачанного места. */
52
+ function onWaiting() {
53
+ isPlaying.value = false;
54
+ isLoaded.value = false;
55
+ }
56
+
57
+ onMounted(() => {
58
+ const video = videoRef.value;
59
+ if (!video) return;
60
+
61
+ video.muted = true;
62
+ video.play().catch(() => {
63
+ isPlaying.value = false;
64
+ });
65
+ });
17
66
  </script>
18
67
 
19
68
  <style lang="scss">
69
+ $loader-height: 4px;
70
+
20
71
  .card-video-preview {
21
72
  position: absolute;
22
73
  inset: 0;
74
+ z-index: $z-index-plus;
75
+ }
76
+
77
+ .card-video-preview__video {
23
78
  width: 100%;
24
79
  height: 100%;
25
- z-index: $z-index-plus;
26
80
  object-fit: cover;
81
+ opacity: 0;
82
+ }
83
+
84
+ /* Пока автоплей не стартовал, браузер рисует поверх видео нативную кнопку плей — держим элемент невидимым */
85
+ .card-video-preview__video.--playing {
86
+ opacity: 1;
87
+ }
88
+
89
+ .card-video-preview__loader {
90
+ position: absolute;
91
+ inset: 0 0 auto;
92
+ height: $loader-height;
93
+ background: $video-card-preview-loader-bg;
94
+ transform: scaleX(var(--preview-progress, 0));
95
+ transform-origin: left center;
96
+ transition: var(--transition) transform, var(--transition) var(--default-transition-duration) opacity;
97
+ }
98
+
99
+ /* Догрузилось или пошло воспроизведение — полоса доезжает до конца и только потом гаснет */
100
+ .card-video-preview__loader.--done {
101
+ opacity: 0;
27
102
  }
28
103
  </style>
@@ -0,0 +1,45 @@
1
+ let isTouchUsed = false;
2
+ let isOutsideTouchWatched = false;
3
+
4
+ /** Хранит id карточки с активным превью: играет только одно, запуск нового гасит предыдущее. */
5
+ export const useVideoPreview = () => {
6
+ const activePreviewId = useState<string | null>('active-video-preview', () => null);
7
+
8
+ const startPreviewByHover = (id: string) => {
9
+ if (isTouchUsed) return;
10
+ activePreviewId.value = id;
11
+ };
12
+
13
+ /** На тач-устройствах превью живёт до запуска следующего, поэтому гасим только по уходу мыши. */
14
+ const stopPreviewByHover = (id: string) => {
15
+ if (isTouchUsed) return;
16
+ if (activePreviewId.value === id) activePreviewId.value = null;
17
+ };
18
+
19
+ /**
20
+ * Гасит превью на любом касании страницы. Слушатель на capture-фазе, поэтому срабатывает раньше обработчика
21
+ * карточки: тап по другой карточке сначала гасит текущее превью, а потом запускает своё, тап по пустому месту — только гасит.
22
+ */
23
+ const watchOutsideTouch = () => {
24
+ if (isOutsideTouchWatched) return;
25
+ isOutsideTouchWatched = true;
26
+
27
+ document.addEventListener('touchstart', () => {
28
+ activePreviewId.value = null;
29
+ }, { capture: true, passive: true });
30
+ };
31
+
32
+ /** Тач-устройства дублируют тап синтетическими mouse-событиями — с первого касания ховерный путь глушим. */
33
+ const startPreviewByTouch = (id: string) => {
34
+ isTouchUsed = true;
35
+ watchOutsideTouch();
36
+ activePreviewId.value = id;
37
+ };
38
+
39
+ return {
40
+ activePreviewId,
41
+ startPreviewByHover,
42
+ stopPreviewByHover,
43
+ startPreviewByTouch,
44
+ };
45
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.849",
4
+ "version": "0.0.850",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {