itube-specs 0.0.913 → 0.0.915

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.
@@ -4,11 +4,34 @@
4
4
 
5
5
  @mixin video-card {
6
6
  display: grid;
7
- row-gap: 12px;
7
+ row-gap: 16px;
8
8
  width: 100%;
9
9
  align-content: start;
10
10
  }
11
11
 
12
+ /**
13
+ Сетка карточек видео: 2 колонки, 3 с xs, 4 с md.
14
+ minmax(0, 1fr) обязателен, не `1fr`: мета-строка карточки истории нешринкуема и задаёт
15
+ колонке min-content, из-за которого грид вылезает за контейнер на телефоне.
16
+ */
17
+ @mixin video-card-grid {
18
+ display: grid;
19
+ gap: 12px;
20
+ grid-template-columns: repeat(2, minmax(0, 1fr));
21
+
22
+ @include from-br(xs) {
23
+ grid-template-columns: repeat(3, minmax(0, 1fr));
24
+ }
25
+
26
+ @include from-br(sm) {
27
+ gap: 24px;
28
+ }
29
+
30
+ @include from-br(md) {
31
+ grid-template-columns: repeat(4, minmax(0, 1fr));
32
+ }
33
+ }
34
+
12
35
  @mixin video-card-poster {
13
36
  position: relative;
14
37
  aspect-ratio: 16/9;
@@ -592,8 +592,6 @@ $label-primary-bg: $color-indigo-600;
592
592
  $label-primary-text: $color-white;
593
593
  $label-gradient-bg: $color-indigo-600;
594
594
  $label-gradient-text: $color-white;
595
- $label-yellow-bg: $color-amber-500;
596
- $label-yellow-text: $mono-black-100;
597
595
  $label-green-bg: $color-green-500;
598
596
  $label-green-text: $color-white;
599
597
  $label-white-bg: $mono-white-75;
@@ -643,8 +643,6 @@ $label-primary-bg: $color-indigo-600;
643
643
  $label-primary-text: $color-white;
644
644
  $label-gradient-bg: $color-indigo-600;
645
645
  $label-gradient-text: $color-white;
646
- $label-yellow-bg: $color-amber-500;
647
- $label-yellow-text: $mono-black-100;
648
646
  $label-green-bg: $color-green-500;
649
647
  $label-green-text: $color-white;
650
648
  $label-white-bg: $mono-white-75;
@@ -135,6 +135,17 @@
135
135
  />
136
136
  </template>
137
137
  <template #items="className">
138
+ <button
139
+ :class="className"
140
+ type="button"
141
+ @click="toggleFavorite(card.id, card.name)"
142
+ >
143
+ <UiIcon
144
+ name="heart-stroke"
145
+ size="16"
146
+ />
147
+ {{ favoritesLabel }}
148
+ </button>
138
149
  <button
139
150
  :class="className"
140
151
  type="button"
@@ -180,7 +191,7 @@ import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
180
191
  const props = defineProps<{
181
192
  card: IPlaylistCard
182
193
  topPlaylists?: boolean
183
- /** Без меню «три точки» — вместе с ним уходят share и report */
194
+ /** Без меню «три точки» — вместе с ним уходят избранное, share и report */
184
195
  noActions?: boolean
185
196
  priority?: boolean
186
197
  }>();
@@ -205,6 +216,23 @@ const userProfileData = computed(() => useState<IProfileData>('profileData').val
205
216
 
206
217
  const { isAuthorized } = useUser(AuthorizationApiService);
207
218
 
219
+ const { isFavorite, toggleFavorite } = usePlaylistCollections();
220
+
221
+ const favorite = computed(() => isFavorite(props.card.id));
222
+
223
+ // Тем же кликом, что переключает коллекцию, меню закрывается. Если менять надпись сразу, она
224
+ // дёргается прямо во время анимации закрытия, поэтому в меню живёт снапшот состояния — он
225
+ // догоняет реальное только к следующему открытию, когда пункт всё равно перерисовывается с нуля.
226
+ const shownFavorite = ref(favorite.value);
227
+
228
+ const favoritesLabel = computed(() => shownFavorite.value
229
+ ? t('video_actions.remove_favorites')
230
+ : t('video_actions.add_favorites'));
231
+
232
+ watch(() => dropdownRef.value?.open, (isOpen) => {
233
+ if (isOpen) shownFavorite.value = favorite.value;
234
+ });
235
+
208
236
  const avatar = computed(() => {
209
237
  if (isAuthorized.value && props.topPlaylists && userProfileData.value?.avatar) {
210
238
  return userProfileData.value.avatar;
@@ -70,10 +70,11 @@ function onImageError() {
70
70
  </script>
71
71
 
72
72
  <style lang="scss">
73
- /* Заход бейджа внутрь круга. На мобиле меньше: аватар там 80px против 96px, а сам
74
- бейдж не уменьшается — при одинаковом отступе он садится заметно глубже. */
75
- $badge-inset: 8px;
76
- $badge-inset--mobile: 4px;
73
+ /* Заход бейджа внутрь круга: отступ от угла карточки подобран так, чтобы центр
74
+ бейджа сел на границу окружности по диагонали — примерно половина снаружи.
75
+ На мобиле аватар 80px против 96px, а бейдж не уменьшается, поэтому отступ меньше. */
76
+ $badge-inset: 3px;
77
+ $badge-inset--mobile: 1px;
77
78
 
78
79
  .card-round {
79
80
  position: relative;
@@ -388,7 +388,7 @@ $dropdown-column: 18px;
388
388
 
389
389
  .card-video__wrapper {
390
390
  display: grid;
391
- grid-template-columns: 1fr $dropdown-column;
391
+ grid-template-columns: minmax(0, 1fr) $dropdown-column;
392
392
  align-items: start;
393
393
  align-content: center;
394
394
  color: $video-card-secondary;
@@ -399,13 +399,13 @@ $dropdown-column: 18px;
399
399
 
400
400
  @include from-cq($layout-breakpoint) {
401
401
  column-gap: 12px;
402
- grid-template-columns: 36px 1fr $dropdown-column;
402
+ grid-template-columns: 36px minmax(0, 1fr) $dropdown-column;
403
403
  }
404
404
  }
405
405
 
406
406
  .--avatar .card-video__wrapper {
407
407
  column-gap: 12px;
408
- grid-template-columns: 36px 1fr $dropdown-column;
408
+ grid-template-columns: 36px minmax(0, 1fr) $dropdown-column;
409
409
  }
410
410
 
411
411
  .--avatar .card-video__title {
@@ -434,7 +434,7 @@ $dropdown-column: 18px;
434
434
  }
435
435
 
436
436
  .--no-channel .card-video__wrapper {
437
- grid-template-columns: 1fr $dropdown-column;
437
+ grid-template-columns: minmax(0, 1fr) $dropdown-column;
438
438
  }
439
439
 
440
440
  .--no-channel .card-video__title {
@@ -452,7 +452,7 @@ $dropdown-column: 18px;
452
452
  }
453
453
 
454
454
  .--mini .card-video__wrapper {
455
- grid-template-columns: 1fr $dropdown-column;
455
+ grid-template-columns: minmax(0, 1fr) $dropdown-column;
456
456
  }
457
457
 
458
458
  /* Без строки канала и тегов вторая строка грида не нужна — иначе аватар и «троеточие» растягиваются. */
@@ -511,6 +511,7 @@ $dropdown-column: 18px;
511
511
  display: flex;
512
512
  align-items: center;
513
513
  grid-row: 2;
514
+ min-width: 0;
514
515
  margin: 6px 0 0;
515
516
  font-weight: 400;
516
517
  color: $video-card-secondary;
@@ -520,6 +521,14 @@ $dropdown-column: 18px;
520
521
  }
521
522
  }
522
523
 
524
+ /* На узкой карточке (две колонки на телефоне) «канал · время» в строку не встают, а нешрикуемое
525
+ время задаёт колонке min-content и разносит грид — оставляем только время просмотра */
526
+ @include to-cq($layout-breakpoint) {
527
+ .card-video__meta .card-video__channel-title {
528
+ display: none;
529
+ }
530
+ }
531
+
523
532
  .card-video__meta .card-video__channel-title + .card-video__watched::before {
524
533
  content: '·';
525
534
  margin-inline: 6px;
@@ -530,6 +539,9 @@ $dropdown-column: 18px;
530
539
  align-items: center;
531
540
  column-gap: 4px;
532
541
  flex-shrink: 0;
542
+ min-width: 0;
543
+ white-space: nowrap;
544
+ overflow: hidden;
533
545
  }
534
546
 
535
547
  .card-video__watched.--remaining {
@@ -72,22 +72,9 @@ const lazyItems = computed(() => props.items.slice(eagerCount.value + IDLE_COUNT
72
72
 
73
73
  <style lang="scss">
74
74
  .grid-videos {
75
- position: relative;
76
- display: grid;
77
- gap: 12px;
78
- grid-template-columns: repeat(2, 1fr);
79
-
80
- @include from-br(xs) {
81
- grid-template-columns: repeat(3, 1fr);
82
- }
83
-
84
- @include from-br(sm) {
85
- gap: 24px;
86
- }
75
+ @include video-card-grid;
87
76
 
88
- @include from-br(md) {
89
- grid-template-columns: repeat(4, 1fr);
90
- }
77
+ position: relative;
91
78
  }
92
79
 
93
80
  .grid-videos__idle,
@@ -260,6 +260,7 @@ router.afterEach(() => {
260
260
 
261
261
  .layout-burger__menu:last-child {
262
262
  padding-bottom: 12px;
263
+ margin-bottom: 4px;
263
264
  }
264
265
 
265
266
  .layout-burger__niche {
@@ -1,11 +1,14 @@
1
1
  <template>
2
2
  <nav class="layout-extra-links">
3
+ <!-- active-class указывает на базовый класс, а не на '': пустую строку NuxtLink считает
4
+ falsy и подставляет дефолты router-link-active, а они зависят от текущего роута и
5
+ ломают гидрацию лениво гидрируемого футера. Дубль класса Vue схлопывает. -->
3
6
  <NuxtLink
4
7
  v-for="(item, index) in additionalLinksItems"
5
8
  :key="`s-additional-link-${index}`"
6
9
  :to="generateLink(item.name)"
7
- active-class=""
8
- exact-active-class=""
10
+ active-class="layout-extra-links__link"
11
+ exact-active-class="layout-extra-links__link"
9
12
  class="layout-extra-links__link"
10
13
  @click="emit('close')"
11
14
  >{{ $t(`additional_link.${item.title}`) }}</NuxtLink>
@@ -1,9 +1,12 @@
1
1
  <template>
2
+ <!-- active-class указывает на базовый класс, а не на '': пустую строку NuxtLink считает
3
+ falsy и подставляет дефолты router-link-active, а они зависят от текущего роута и
4
+ ломают гидрацию лениво гидрируемого футера. Дубль класса Vue схлопывает. -->
2
5
  <NuxtLink
3
6
  :class="{'layout-logo--glow': glow}"
4
7
  :to="generateLink('/')"
5
- active-class=""
6
- exact-active-class=""
8
+ active-class="layout-logo"
9
+ exact-active-class="layout-logo"
7
10
  class="layout-logo"
8
11
  @click="onLogoClick"
9
12
  >
@@ -199,8 +199,12 @@ const ageText = computed(() => {
199
199
  grid-template-columns: auto minmax(0, 1fr) auto;
200
200
  column-gap: 16px;
201
201
  row-gap: 12px;
202
- align-items: start;
202
+ align-items: center;
203
203
  margin-bottom: 20px;
204
+
205
+ @include from-br(sm) {
206
+ align-items: start;
207
+ }
204
208
  }
205
209
 
206
210
  .library-feed__header.--without-avatar {
@@ -11,7 +11,7 @@
11
11
  <script setup lang="ts">
12
12
  withDefaults(
13
13
  defineProps<{
14
- theme?: 'default' | 'primary' | 'primary-gradient' | 'yellow' | 'green' | 'white' | 'black' | 'backdrop' | 'secondary' | 'success' | 'blur' | 'grey'
14
+ theme?: 'default' | 'primary' | 'primary-gradient' | 'green' | 'white' | 'black' | 'backdrop' | 'secondary' | 'success' | 'blur' | 'grey'
15
15
  text: string | number
16
16
  }>(), {
17
17
  theme: 'default'
@@ -47,15 +47,6 @@ withDefaults(
47
47
  color: $label-gradient-text;
48
48
  }
49
49
 
50
- .ui-label.--yellow {
51
- @include font-3xs;
52
-
53
- background: $label-yellow-bg;
54
- color: $label-yellow-text;
55
- height: 15px;
56
- display: inline-block;
57
- }
58
-
59
50
  .ui-label.--green {
60
51
  background-color: $label-green-bg;
61
52
  color: $label-green-text;
@@ -4,12 +4,6 @@
4
4
  <header class="view-video__header">
5
5
  <h1 class="view-video__title">
6
6
  <span class="_truncate">{{ data.title }}</span>
7
- <UiLabel
8
- v-if="data.quality"
9
- :text="data.quality"
10
- class="view-video__title-label"
11
- theme="yellow"
12
- />
13
7
  </h1>
14
8
  <div class="view-video__header-wrapper">
15
9
  <UiExpandRow
@@ -248,12 +242,6 @@ function getModelChips(model: IVideoData['models'][number], index: number): IChi
248
242
  letter-spacing: var(--tracking-wide);
249
243
  }
250
244
 
251
- .view-video__title-label {
252
- display: inline;
253
- vertical-align: middle;
254
- margin-left: 12px;
255
- }
256
-
257
245
  .view-video__header-wrapper {
258
246
  margin-bottom: 16px;
259
247
  }
@@ -0,0 +1,63 @@
1
+ // @vitest-environment nuxt
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import { usePlaylistCollections } from '../use-playlist-collections';
4
+ import { useVideoCollections } from '../use-video-collections';
5
+ import { useSnackbar } from '../use-snackbar';
6
+
7
+ describe('usePlaylistCollections', () => {
8
+ beforeEach(() => {
9
+ useNuxtApp().payload.state = {};
10
+ vi.useFakeTimers();
11
+ useSnackbar().resetSnackbar();
12
+ });
13
+
14
+ afterEach(() => {
15
+ vi.useRealTimers();
16
+ });
17
+
18
+ it('добавляет в избранное и показывает снекбар с кнопкой отмены', () => {
19
+ const { favorites, isFavorite, toggleFavorite } = usePlaylistCollections();
20
+ const { snackbarText, snackbarIcon, snackbarHighlight, snackbarActionText } = useSnackbar();
21
+
22
+ toggleFavorite('playlist-1', 'Playlist name');
23
+
24
+ expect(favorites.value).toEqual([ 'playlist-1' ]);
25
+ expect(isFavorite('playlist-1')).toBe(true);
26
+ expect(snackbarText.value).toBe('added');
27
+ expect(snackbarIcon.value).toBe('heart');
28
+ expect(snackbarHighlight.value).toBe('Playlist name');
29
+ expect(snackbarActionText.value).toBe('undo');
30
+ });
31
+
32
+ it('удаление показывает снекбар «removed»', () => {
33
+ const { favorites, toggleFavorite } = usePlaylistCollections();
34
+ const { snackbarText } = useSnackbar();
35
+
36
+ toggleFavorite('playlist-1', 'Playlist name');
37
+ toggleFavorite('playlist-1', 'Playlist name');
38
+
39
+ expect(favorites.value).toEqual([]);
40
+ expect(snackbarText.value).toBe('removed');
41
+ });
42
+
43
+ it('отмена возвращает список и не показывает новый снекбар', () => {
44
+ const { favorites, toggleFavorite } = usePlaylistCollections();
45
+ const { runSnackbarAction, snackbarText } = useSnackbar();
46
+
47
+ toggleFavorite('playlist-2', 'Another name');
48
+ runSnackbarAction();
49
+
50
+ expect(favorites.value).toEqual([]);
51
+ expect(snackbarText.value).toBe('');
52
+ });
53
+
54
+ it('избранные плейлисты не смешиваются с избранными видео', () => {
55
+ const { favorites: playlistFavorites, toggleFavorite: togglePlaylist } = usePlaylistCollections();
56
+ const { favorites: videoFavorites } = useVideoCollections();
57
+
58
+ togglePlaylist('playlist-3', 'Third name');
59
+
60
+ expect(playlistFavorites.value).toEqual([ 'playlist-3' ]);
61
+ expect(videoFavorites.value).toEqual([]);
62
+ });
63
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Заглушка избранных плейлистов — механика та же, что у видео (см. `useVideoCollections`),
3
+ * но список свой: плейлист не ролик, и на странице избранного они не должны смешиваться.
4
+ * Ручек в API пока нет, поэтому состояние живёт только на клиенте и сбрасывается при перезагрузке.
5
+ */
6
+ export const usePlaylistCollections = () => {
7
+ const favorites = useState<string[]>('playlist-collections-favorites', () => []);
8
+
9
+ const { show } = useSnackbar();
10
+
11
+ /** Возвращает true, если плейлист добавлен, и false — если убран. */
12
+ function toggle(id: string) {
13
+ const added = !favorites.value.includes(id);
14
+
15
+ favorites.value = added
16
+ ? [ ...favorites.value, id ]
17
+ : favorites.value.filter(item => item !== id);
18
+
19
+ return added;
20
+ }
21
+
22
+ const isFavorite = (id: string) => favorites.value.includes(id);
23
+
24
+ // Снекбар показывается только на исходное действие: откат возвращает список как был,
25
+ // повторно сообщать об этом нечего — иначе отмена сама выглядит как новое изменение.
26
+ const toggleFavorite = (id: string, name = '') => {
27
+ show(toggle(id) ? 'added' : 'removed', {
28
+ icon: 'heart',
29
+ highlight: name,
30
+ onAction: () => toggle(id),
31
+ });
32
+ };
33
+
34
+ return {
35
+ favorites,
36
+ isFavorite,
37
+ toggleFavorite,
38
+ };
39
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.913",
4
+ "version": "0.0.915",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
package/pages/history.vue CHANGED
@@ -238,19 +238,9 @@ const breadcrumbs = [
238
238
  }
239
239
 
240
240
  .history__grid {
241
- position: relative;
242
- display: grid;
243
- gap: 12px;
244
- grid-template-columns: repeat(2, 1fr);
245
-
246
- @include from-br(sm) {
247
- gap: 24px;
248
- grid-template-columns: repeat(3, 1fr);
249
- }
241
+ @include video-card-grid;
250
242
 
251
- @include from-br(md) {
252
- grid-template-columns: repeat(4, 1fr);
253
- }
243
+ position: relative;
254
244
  }
255
245
 
256
246
  .ui-pagination.history__pagination {
@@ -30,7 +30,7 @@
30
30
 
31
31
  <UiLink
32
32
  :to="generateLink('/history')"
33
- theme="secondary"
33
+ theme="simple"
34
34
  >
35
35
  {{ t('library.history') }}
36
36
  <UiIcon
@@ -91,7 +91,7 @@
91
91
 
92
92
  <UiLink
93
93
  :to="generateLink('/following')"
94
- theme="secondary"
94
+ theme="simple"
95
95
  >
96
96
  {{ t('library.whats_new', { count: newTotal }) }}
97
97
  <UiIcon
@@ -5,7 +5,6 @@ describe('cleanVideoCard', () => {
5
5
  it('полные данные', () => {
6
6
  const raw = {
7
7
  guid: 'v-1',
8
- quality: '1080p',
9
8
  duration: 600,
10
9
  title: 'Video Title',
11
10
  views: 1000,
@@ -27,7 +26,6 @@ describe('cleanVideoCard', () => {
27
26
  };
28
27
  const result = cleanVideoCard(raw as any);
29
28
  expect(result.guid).toBe('v-1');
30
- expect(result.quality).toBe('1080p');
31
29
  expect(result.isNew).toBe(true);
32
30
  expect(result.channelName).toBe('Channel');
33
31
  expect(result.channelAvatar).toBe('https://img.com/ch.jpg');
@@ -44,7 +42,6 @@ describe('cleanVideoCard', () => {
44
42
  it('пустые данные → дефолты', () => {
45
43
  const result = cleanVideoCard({} as any);
46
44
  expect(result.guid).toBe('');
47
- expect(result.quality).toBe('');
48
45
  expect(result.duration).toBe(0);
49
46
  expect(result.views).toBe(0);
50
47
  expect(result.isNew).toBe(false);
@@ -6,7 +6,6 @@ describe('cleanVideoData', () => {
6
6
  const raw = {
7
7
  guid: 'v-1',
8
8
  md5: 'abc',
9
- quality: '1080p',
10
9
  duration: 600,
11
10
  title: 'Title',
12
11
  header: 'Header',
@@ -4,7 +4,6 @@ import { ThumbSize } from '../../constants/thumb-size';
4
4
  /** Нормализует сырую карточку видео к типу IVideoCard. */
5
5
  export const cleanVideoCard = (card: IRawVideoCard): IVideoCard => ({
6
6
  guid: card.guid || '',
7
- quality: card.quality || '',
8
7
  duration: card.duration || 0,
9
8
  title: card.title || '',
10
9
  views: card.views || 0,
@@ -39,7 +39,6 @@ const cleanHotspots = (raw: string[] | null): number[] | null => {
39
39
  export const cleanVideoData = (data: IRawVideoData): IVideoData => ({
40
40
  guid: data.guid || '',
41
41
  md5: data.md5 || '',
42
- quality: data.quality || '',
43
42
  duration: data.duration || 0,
44
43
  title: data.title || '',
45
44
  header: data.header || '',
package/stubs/history.ts CHANGED
@@ -11,7 +11,6 @@ export type HistoryStubCard = {
11
11
  duration: number;
12
12
  thumbUrls: IThumbUrls;
13
13
  previewUrl: string;
14
- quality?: string;
15
14
  channelName?: string;
16
15
  channelAvatar?: string;
17
16
  /** Доля просмотренного, 0–100 */
package/stubs/videos.ts CHANGED
@@ -5,7 +5,6 @@ export type StubVideo = {
5
5
  id: string;
6
6
  title: string;
7
7
  duration: number;
8
- quality?: string;
9
8
  thumbUrls: IThumbUrls;
10
9
  previewUrl: string;
11
10
  channelName?: string;
@@ -15,7 +14,7 @@ export type StubVideo = {
15
14
  /** Общий пул роликов для всех заглушек — порядок фиксирован, на него завязаны наборы-надстройки. */
16
15
  export const VIDEO_POOL: StubVideo[] = [
17
16
  { id: '445839845', title: 'Drew Sebastian Barebacks Nick Fitt: Muscle Daddy Tops Athletic Bottom on the Couch', duration: 486, thumbUrls: { webp: { '320x180': 'https://nlt01.gay.cool/a/8/d/a8dc253cad705d688508de096435a989/thumbs/320x180/3.webp', '480x270': 'https://nlt01.gay.cool/a/8/d/a8dc253cad705d688508de096435a989/thumbs/480x270/3.webp' } }, channelName: 'Icon Male', channelAvatar: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', previewUrl: 'https://nlp02.gay.cool/a/8/d/a8dc253cad705d688508de096435a989/preview/video.mp4' },
18
- { id: '171708405', title: 'Bareback bathroom fisting with black latex gloves, rimming, piss play and anal gape', duration: 358, quality: '1080', thumbUrls: { webp: { '320x180': 'https://nlt03.gay.cool/b/7/5/b75c1bf33571126845502e2b9bb5d687/thumbs/320x180/33.webp', '480x270': 'https://nlt03.gay.cool/b/7/5/b75c1bf33571126845502e2b9bb5d687/thumbs/480x270/33.webp' } }, previewUrl: 'https://nlp02.gay.cool/b/7/5/b75c1bf33571126845502e2b9bb5d687/preview/video.mp4' },
17
+ { id: '171708405', title: 'Bareback bathroom fisting with black latex gloves, rimming, piss play and anal gape', duration: 358, thumbUrls: { webp: { '320x180': 'https://nlt03.gay.cool/b/7/5/b75c1bf33571126845502e2b9bb5d687/thumbs/320x180/33.webp', '480x270': 'https://nlt03.gay.cool/b/7/5/b75c1bf33571126845502e2b9bb5d687/thumbs/480x270/33.webp' } }, previewUrl: 'https://nlp02.gay.cool/b/7/5/b75c1bf33571126845502e2b9bb5d687/preview/video.mp4' },
19
18
  { id: '374534215', title: 'Jake Davis Takes Bareback Anal in Blue Harness from Athletic Partner', duration: 487, thumbUrls: { webp: { '320x180': 'https://nlt02.gay.cool/2/4/6/246f577796c7c955b8df94f6454de2a5/thumbs/320x180/12.webp', '480x270': 'https://nlt02.gay.cool/2/4/6/246f577796c7c955b8df94f6454de2a5/thumbs/480x270/12.webp' } }, channelName: 'Icon Male', channelAvatar: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', previewUrl: 'https://nlp05.gay.cool/2/4/6/246f577796c7c955b8df94f6454de2a5/preview/video.mp4' },
20
19
  { id: '3377550', title: 'Rego Bello and Devin Adams bareback bedroom flip: reciprocal oral then Devin tops Rego', duration: 426, thumbUrls: { webp: { '320x180': 'https://nlt01.gay.cool/3/e/b/3eb0f19f1e600a98a7f5a5e51edc4b46/thumbs/320x180/33.webp', '480x270': 'https://nlt01.gay.cool/3/e/b/3eb0f19f1e600a98a7f5a5e51edc4b46/thumbs/480x270/33.webp' } }, previewUrl: 'https://nlp05.gay.cool/3/e/b/3eb0f19f1e600a98a7f5a5e51edc4b46/preview/video.mp4' },
21
20
  { id: '443170695', title: 'Johnny Rapid and Malakai White lead four-man hotel bareback with Max Lorde and Kyler Drayke', duration: 487, thumbUrls: { webp: { '320x180': 'https://nlt04.gay.cool/9/f/f/9ff067b9ea8b157ff2478667423ff9bb/thumbs/320x180/29.webp', '480x270': 'https://nlt04.gay.cool/9/f/f/9ff067b9ea8b157ff2478667423ff9bb/thumbs/480x270/29.webp' } }, channelName: 'Johnny Rapid', channelAvatar: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', previewUrl: 'https://nlp01.gay.cool/9/f/f/9ff067b9ea8b157ff2478667423ff9bb/preview/video.mp4' },
@@ -25,5 +24,5 @@ export const VIDEO_POOL: StubVideo[] = [
25
24
  { id: '444068845', title: 'Dean Young Bareback Flip Fuck: Versatile Anal, Deepthroat and Rimming in the Bedroom', duration: 604, thumbUrls: { webp: { '320x180': 'https://nlt03.gay.cool/7/d/a/7daacbe876b51c7cdc50101b0d504be8/thumbs/320x180/16.webp', '480x270': 'https://nlt03.gay.cool/7/d/a/7daacbe876b51c7cdc50101b0d504be8/thumbs/480x270/16.webp' } }, previewUrl: 'https://nlp03.gay.cool/7/d/a/7daacbe876b51c7cdc50101b0d504be8/preview/video.mp4' },
26
25
  { id: '374968825', title: 'Brody Fox sucks and rides daddy Cain Marko bareback', duration: 497, thumbUrls: { webp: { '320x180': 'https://nlt01.gay.cool/9/e/2/9e23f59e882e860da3ab2400d93ac73e/thumbs/320x180/10.webp', '480x270': 'https://nlt01.gay.cool/9/e/2/9e23f59e882e860da3ab2400d93ac73e/thumbs/480x270/10.webp' } }, previewUrl: 'https://nlp03.gay.cool/9/e/2/9e23f59e882e860da3ab2400d93ac73e/preview/video.mp4' },
27
26
  { id: '377844995', title: 'Rick Lovatelli: blond twink dominated, deepthroats and gets barebacked by a suited bearded top', duration: 1627, thumbUrls: { webp: { '320x180': 'https://nlt05.gay.cool/f/1/b/f1b09d77a2e23f4b4696d97169614099/thumbs/320x180/25.webp', '480x270': 'https://nlt05.gay.cool/f/1/b/f1b09d77a2e23f4b4696d97169614099/thumbs/480x270/25.webp' } }, previewUrl: 'https://nlp01.gay.cool/f/1/b/f1b09d77a2e23f4b4696d97169614099/preview/video.mp4' },
28
- { id: '375132285', title: 'Viktor Rom and Jordan Fox bareback blue-haired bottom in bedroom threesome', duration: 800, quality: '1080', thumbUrls: { webp: { '320x180': 'https://nlt03.gay.cool/5/f/b/5fbcdd007476cd0a1e9c85abc9fc0042/thumbs/320x180/1.webp', '480x270': 'https://nlt03.gay.cool/5/f/b/5fbcdd007476cd0a1e9c85abc9fc0042/thumbs/480x270/1.webp' } }, previewUrl: 'https://nlp04.gay.cool/5/f/b/5fbcdd007476cd0a1e9c85abc9fc0042/preview/video.mp4' },
27
+ { id: '375132285', title: 'Viktor Rom and Jordan Fox bareback blue-haired bottom in bedroom threesome', duration: 800, thumbUrls: { webp: { '320x180': 'https://nlt03.gay.cool/5/f/b/5fbcdd007476cd0a1e9c85abc9fc0042/thumbs/320x180/1.webp', '480x270': 'https://nlt03.gay.cool/5/f/b/5fbcdd007476cd0a1e9c85abc9fc0042/thumbs/480x270/1.webp' } }, previewUrl: 'https://nlp04.gay.cool/5/f/b/5fbcdd007476cd0a1e9c85abc9fc0042/preview/video.mp4' },
29
28
  ];
@@ -37,7 +37,7 @@ interface IChannelVideo {
37
37
  export interface IRawVideoCard {
38
38
  guid: string;
39
39
  md5: string; // unused
40
- quality: string;
40
+ quality: string; // unused
41
41
  orientation: string; // unused
42
42
  duration: number;
43
43
  created: number; // unused
@@ -4,7 +4,7 @@ import type { ITimelineGroup } from '../video-data';
4
4
  export interface IRawVideoData {
5
5
  guid: string;
6
6
  md5: string;
7
- quality: string;
7
+ quality: string; // unused
8
8
  orientation: string; // unused
9
9
  duration: number;
10
10
  created: number; // unused
@@ -2,7 +2,6 @@ import type { IThumbUrls } from '~/types/thumbs-urls';
2
2
 
3
3
  export interface IVideoCard {
4
4
  guid: string;
5
- quality?: string;
6
5
  duration: number;
7
6
  title: string;
8
7
  views?: number;
@@ -32,7 +32,6 @@ export interface ISceneGroup {
32
32
  export interface IVideoData {
33
33
  guid: string;
34
34
  md5: string;
35
- quality: string;
36
35
  duration: number;
37
36
  title: string;
38
37
  header: string;