itube-specs 0.0.899 → 0.0.902

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.
Files changed (40) hide show
  1. package/assets/icons/common/clock-filled.svg +1 -0
  2. package/assets/scss/mixins/_breakpoints.scss +3 -2
  3. package/assets/scss/themes/dark/_theme.scss +17 -23
  4. package/assets/scss/themes/light/_theme.scss +17 -23
  5. package/assets/scss/vars/_palette.scss +1 -0
  6. package/components/card/category.vue +4 -8
  7. package/components/card/dropdown/index.vue +47 -9
  8. package/components/card/dropdown/trigger.vue +1 -0
  9. package/components/card/model.vue +1 -1
  10. package/components/card/playlist.vue +15 -6
  11. package/components/card/round.vue +3 -3
  12. package/components/card/video/actions.vue +123 -0
  13. package/components/card/video/index.vue +149 -30
  14. package/components/filter/selects.vue +1 -1
  15. package/components/info/grid.vue +1 -1
  16. package/components/layout/header-user.vue +1 -1
  17. package/components/library/feed.vue +65 -24
  18. package/components/library/follows.vue +8 -44
  19. package/components/library/item.vue +4 -4
  20. package/components/player/like.vue +1 -1
  21. package/components/player/playlist.vue +2 -2
  22. package/components/report/main.vue +20 -5
  23. package/components/ui/checkbox.vue +1 -1
  24. package/components/ui/dropdown.vue +1 -1
  25. package/components/ui/input.vue +2 -2
  26. package/components/ui/radio.vue +1 -1
  27. package/components/ui/toggle.vue +1 -1
  28. package/composables/use-report-popup.ts +26 -3
  29. package/composables/use-video-collections.ts +30 -0
  30. package/lib/scheme/profile-navigation-items.ts +1 -1
  31. package/package.json +1 -1
  32. package/pages/history.vue +12 -36
  33. package/pages/library/categories.vue +2 -1
  34. package/pages/library/channels.vue +1 -1
  35. package/pages/library/index.vue +3 -3
  36. package/pages/library/models.vue +4 -4
  37. package/stubs/library-follows.ts +19 -16
  38. package/types/library-feed.d.ts +6 -0
  39. package/types/video-card.d.ts +4 -0
  40. package/components/card/history.vue +0 -359
@@ -1,4 +1,4 @@
1
- <!-- Модалка жалобы на видео: 2 шага (причина + анкета) с reCAPTCHA -->
1
+ <!-- Модалка жалобы на видео или плейлист: 2 шага (причина + анкета) с reCAPTCHA -->
2
2
  <template>
3
3
  <transition mode="out-in">
4
4
  <UiPopup
@@ -11,7 +11,13 @@
11
11
  >
12
12
  <template #title>{{ activeStep !== 'list' && activeCategory ? t(`report_form.${activeCategory.title}`) : t('report') }}</template>
13
13
  <template #fixedContent>
14
+ <CardPlaylistMini
15
+ v-if="reportTargetType === 'playlist'"
16
+ :card="reportedPlaylistCard"
17
+ class="report-main__video-card"
18
+ />
14
19
  <CardVideoMini
20
+ v-else
15
21
  :card="reportedVideoCard"
16
22
  class="report-main__video-card"
17
23
  />
@@ -163,7 +169,16 @@ const form = ref<IReportForm>({
163
169
  data: {},
164
170
  });
165
171
 
166
- const { isReportPopupOpen, closeReportPopup, reportedVideoCard, onBack } = useReportPopup();
172
+ const {
173
+ isReportPopupOpen,
174
+ closeReportPopup,
175
+ reportedVideoCard,
176
+ reportedPlaylistCard,
177
+ reportTargetType,
178
+ reportedUrl,
179
+ reportedGuid,
180
+ onBack,
181
+ } = useReportPopup();
167
182
 
168
183
  const showBack = computed(() => activeStep.value !== 'list' || !!onBack.value);
169
184
 
@@ -196,7 +211,7 @@ watch(reasonValue, (val) => {
196
211
  if (val) errorRadio.value = false;
197
212
  });
198
213
 
199
- const videoGuid = computed(() => reportedVideoCard.value.guid);
214
+ const reportGuid = computed(() => reportedGuid.value);
200
215
 
201
216
  const loading = ref(false);
202
217
 
@@ -294,14 +309,14 @@ async function submit() {
294
309
  const baseURL = import.meta.client ? window.location.origin : '';
295
310
  loading.value = true;
296
311
  form.value.token = await getRecaptchaToken('contact_form');
297
- form.value.url = `${baseURL}/videos/${reportedVideoCard.value.id}`;
312
+ form.value.url = `${baseURL}${reportedUrl.value}`;
298
313
 
299
314
  if (reasonValue.value) {
300
315
  form.value.data[ 'reason' ] = reasonValue.value;
301
316
  }
302
317
  emit('submit', {
303
318
  form: form.value,
304
- guid: videoGuid.value
319
+ guid: reportGuid.value
305
320
  });
306
321
  closeReportPopup();
307
322
  resetForm();
@@ -63,7 +63,7 @@ function onChange(event: Event) {
63
63
  column-gap: 8px;
64
64
  grid-template-columns: auto 1fr;
65
65
 
66
- @include from-br(md) {
66
+ @media (hover: hover) {
67
67
  cursor: pointer;
68
68
 
69
69
  &:is(:hover, :focus-visible) {
@@ -161,7 +161,7 @@ const getMaxHeight = computed(() => {
161
161
  return props.maxHeight ? `${props.maxHeight}px` : 'none';
162
162
  });
163
163
 
164
- defineExpose({ openDropdown });
164
+ defineExpose({ openDropdown, open });
165
165
  </script>
166
166
 
167
167
  <style lang="scss">
@@ -302,13 +302,13 @@ const normalizedValue = computed(() => {
302
302
  height: 20px;
303
303
  color: var(--input-icon, #{$input-icon});
304
304
 
305
- @include from-br(sm) {
305
+ @media (hover: hover) {
306
306
  transition: var(--transition) color;
307
307
  }
308
308
  }
309
309
 
310
310
  .ui-input__button:is(:hover, :focus-visible) .ui-input__button-icon {
311
- @include from-br(sm) {
311
+ @media (hover: hover) {
312
312
  color: var(--input-button-text--hover, #{$input-button-text--hover});
313
313
  }
314
314
  }
@@ -60,7 +60,7 @@ const isChecked = computed(() => properties.modelValue === properties.value);
60
60
  column-gap: 8px;
61
61
  justify-content: start;
62
62
 
63
- @include from-br(md) {
63
+ @media (hover: hover) {
64
64
  cursor: pointer;
65
65
 
66
66
  &:is(:hover, :focus-visible) {
@@ -48,7 +48,7 @@ $toggler-size: 16px;
48
48
  position: relative;
49
49
  user-select: none;
50
50
 
51
- @include from-br(md) {
51
+ @media (hover: hover) {
52
52
  cursor: pointer;
53
53
 
54
54
  &:is(:hover, :focus-visible) {
@@ -1,14 +1,32 @@
1
- import type { IVideoCard } from '../types';
1
+ import type { IPlaylistCard, IVideoCard } from '../types';
2
2
 
3
- /** Управляет состоянием попапа жалобы на видео. */
3
+ export type ReportTargetType = 'video' | 'playlist';
4
+
5
+ /** Управляет состоянием попапа жалобы на видео или плейлист. */
4
6
  export const useReportPopup = () => {
5
7
  const isReportPopupOpen = useState('report-popup-open', () => false);
6
8
  const reportedVideoCard = useState<IVideoCard>('report-popup-card', () => ({} as IVideoCard));
9
+ const reportedPlaylistCard = useState<IPlaylistCard>('report-popup-playlist-card', () => ({} as IPlaylistCard));
10
+ const reportTargetType = useState<ReportTargetType>('report-popup-target-type', () => 'video');
11
+ const reportedUrl = useState('report-popup-url', () => '');
12
+ const reportedGuid = useState('report-popup-guid', () => '');
7
13
  const onBack = useState<(() => void) | null>('report-popup-on-back', () => null);
8
14
 
9
15
  const openReportPopup = (card: IVideoCard, onBackCallback?: () => void) => {
10
- isReportPopupOpen.value = true;
16
+ reportTargetType.value = 'video';
11
17
  reportedVideoCard.value = card;
18
+ reportedUrl.value = `/videos/${card.id}`;
19
+ reportedGuid.value = card.guid;
20
+ isReportPopupOpen.value = true;
21
+ onBack.value = onBackCallback ?? null;
22
+ };
23
+
24
+ const openPlaylistReportPopup = (card: IPlaylistCard, onBackCallback?: () => void) => {
25
+ reportTargetType.value = 'playlist';
26
+ reportedPlaylistCard.value = card;
27
+ reportedUrl.value = card.firstVideoId ? `/playlists/${card.id}/${card.firstVideoId}` : `/playlists/${card.id}`;
28
+ reportedGuid.value = card.id;
29
+ isReportPopupOpen.value = true;
12
30
  onBack.value = onBackCallback ?? null;
13
31
  };
14
32
 
@@ -20,8 +38,13 @@ export const useReportPopup = () => {
20
38
  return {
21
39
  isReportPopupOpen,
22
40
  openReportPopup,
41
+ openPlaylistReportPopup,
23
42
  closeReportPopup,
24
43
  reportedVideoCard,
44
+ reportedPlaylistCard,
45
+ reportTargetType,
46
+ reportedUrl,
47
+ reportedGuid,
25
48
  onBack,
26
49
  };
27
50
  };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Заглушка коллекций видео — избранное и «смотреть позже».
3
+ * Ручек в API пока нет, поэтому состояние живёт только на клиенте и сбрасывается при перезагрузке.
4
+ * Когда появятся эндпоинты, тут же меняем toggle-и на запросы, интерфейс наружу остаётся тем же.
5
+ */
6
+ export const useVideoCollections = () => {
7
+ const favorites = useState<string[]>('video-collections-favorites', () => []);
8
+ const watchLater = useState<string[]>('video-collections-watch-later', () => []);
9
+
10
+ function toggle(list: Ref<string[]>, guid: string) {
11
+ list.value = list.value.includes(guid)
12
+ ? list.value.filter(item => item !== guid)
13
+ : [ ...list.value, guid ];
14
+ }
15
+
16
+ const isFavorite = (guid: string) => favorites.value.includes(guid);
17
+ const isWatchLater = (guid: string) => watchLater.value.includes(guid);
18
+
19
+ const toggleFavorite = (guid: string) => toggle(favorites, guid);
20
+ const toggleWatchLater = (guid: string) => toggle(watchLater, guid);
21
+
22
+ return {
23
+ favorites,
24
+ watchLater,
25
+ isFavorite,
26
+ isWatchLater,
27
+ toggleFavorite,
28
+ toggleWatchLater,
29
+ };
30
+ };
@@ -7,7 +7,7 @@ export const profileNavigationItems = [
7
7
  },
8
8
  {
9
9
  value: '/user/profile?edit=true',
10
- title: 'library',
10
+ title: 'navigation.library',
11
11
  icon: 'library',
12
12
  group: 1,
13
13
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.899",
4
+ "version": "0.0.902",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
package/pages/history.vue CHANGED
@@ -49,23 +49,23 @@
49
49
  :key="group.key"
50
50
  class="history__group"
51
51
  >
52
- <header class="history__group-header">
53
- <UiIcon
54
- name="date"
55
- size="16"
56
- class="history__group-icon"
57
- />
58
- <h2 class="history__group-title">{{ getGroupTitle(group) }}</h2>
59
- <span class="history__group-count">({{ t('plural.videos', group.items.length) }})</span>
60
- </header>
52
+ <UiSectionTitle
53
+ :title="getGroupTitle(group)"
54
+ :count="n(group.items.length, 'compact')"
55
+ class="history__group-header"
56
+ icon="video-camera"
57
+ title-tag="h2"
58
+ size="xl"
59
+ />
61
60
 
62
61
  <div class="history__grid">
63
- <CardHistory
62
+ <CardVideo
64
63
  v-for="(item, index) in group.items"
65
64
  :key="`history-${item.guid}`"
66
65
  :card="item"
67
66
  :priority="groupIndex === 0 && index < 4"
68
67
  :loading="groupIndex === 0 && index < 4 ? 'eager' : 'lazy'"
68
+ history
69
69
  @share="onShare"
70
70
  @remove="onRemove"
71
71
  />
@@ -124,14 +124,13 @@
124
124
  <script setup lang="ts">
125
125
  import { historyPeriods } from '../lib';
126
126
  import { getHistoryStub } from '../stubs/history';
127
- import type { HistoryStubCard } from '../stubs/history';
128
127
  import { groupByWatchedDay } from '../utils/history/group-by-watched-day';
129
128
  import type { HistoryDayGroup } from '../utils/history/group-by-watched-day';
130
129
  import type { IBreadcrumbItem, IVideoCard } from '../types';
131
130
 
132
131
  const PER_PAGE = 24;
133
132
 
134
- const { t, locale } = useI18n();
133
+ const { t, n, locale } = useI18n();
135
134
  const route = useRoute();
136
135
 
137
136
  const isFilterOpen = ref(false);
@@ -169,7 +168,7 @@ function getGroupTitle(group: HistoryDayGroup) {
169
168
  });
170
169
  }
171
170
 
172
- function onRemove(card: HistoryStubCard) {
171
+ function onRemove(card: IVideoCard) {
173
172
  removedGuids.value = [...removedGuids.value, card.guid];
174
173
  }
175
174
 
@@ -235,32 +234,9 @@ const breadcrumbs = [
235
234
  }
236
235
 
237
236
  .history__group-header {
238
- display: flex;
239
- align-items: center;
240
- column-gap: 8px;
241
237
  margin-bottom: 16px;
242
238
  }
243
239
 
244
- .history__group-icon {
245
- flex-shrink: 0;
246
- color: $page-history-group-icon;
247
- }
248
-
249
- .history__group-title {
250
- @include font-lg;
251
-
252
- margin: 0;
253
- font-weight: 600;
254
- color: $page-history-group-title;
255
- }
256
-
257
- .history__group-count {
258
- @include font-sm;
259
-
260
- font-weight: 400;
261
- color: $page-history-group-count;
262
- }
263
-
264
240
  .history__grid {
265
241
  position: relative;
266
242
  display: grid;
@@ -10,6 +10,7 @@
10
10
  :items="items"
11
11
  :title="t('library.my_categories')"
12
12
  icon="grid"
13
+ without-avatar
13
14
  />
14
15
  </div>
15
16
  </template>
@@ -29,7 +30,7 @@ const items = computed<ILibraryFeedItem[]>(() => getLibraryCategoriesStub().map(
29
30
  newCount: category.newCount,
30
31
  meta: [
31
32
  { icon: 'video-camera', text: String(category.videosCount) },
32
- { icon: 'date', text: t('library.updated_at', { date: formatDate(category.lastVideoAddedAt) }) },
33
+ { icon: 'date', text: t('library.updated_at', { date: formatDate(category.lastVideoAddedAt) }), desktopOnly: true },
33
34
  ],
34
35
  videosCount: category.videosCount,
35
36
  // Нечего показать в новинках — лента откатывается на лучшее за неделю
@@ -32,7 +32,7 @@ const items = computed<ILibraryFeedItem[]>(() => getLibraryChannelsStub().map(ch
32
32
  meta: [
33
33
  { icon: 'video-camera', text: String(channel.videosCount) },
34
34
  { icon: 'user-group', text: t('library.followers', { count: n(channel.followersCount, 'compact') }) },
35
- { icon: 'date', text: t('library.updated_at', { date: formatDate(channel.lastVideoAddedAt) }) },
35
+ { icon: 'date', text: t('library.updated_at', { date: formatDate(channel.lastVideoAddedAt) }), desktopOnly: true },
36
36
  ],
37
37
  videosCount: channel.videosCount,
38
38
  videos: channel.videos,
@@ -41,12 +41,13 @@
41
41
  </header>
42
42
 
43
43
  <div class="library__continue">
44
- <CardHistory
44
+ <CardVideo
45
45
  v-for="item in continueWatching"
46
46
  :key="`continue-${item.guid}`"
47
47
  :card="item"
48
48
  loading="eager"
49
49
  priority
50
+ history
50
51
  remaining
51
52
  @share="onShare"
52
53
  @remove="onRemove"
@@ -134,7 +135,6 @@
134
135
 
135
136
  <script setup lang="ts">
136
137
  import { getHistoryStub } from '../../stubs/history';
137
- import type { HistoryStubCard } from '../../stubs/history';
138
138
  import { libraryStub } from '../../stubs/library';
139
139
  import type { IBreadcrumbItem, IVideoCard } from '../../types';
140
140
 
@@ -230,7 +230,7 @@ const following = computed<LibraryLink[]>(() => [
230
230
  },
231
231
  ]);
232
232
 
233
- function onRemove(card: HistoryStubCard) {
233
+ function onRemove(card: IVideoCard) {
234
234
  removedGuids.value = [...removedGuids.value, card.guid];
235
235
  }
236
236
 
@@ -1,4 +1,4 @@
1
- <!-- Подписки на моделей: аватар, теги, лента последних роликов. Данные пока заглушка -->
1
+ <!-- Подписки на моделей: аватар, параметры (тип/телосложение/этнос), лента последних роликов. Данные пока заглушка -->
2
2
  <template>
3
3
  <div class="_page _container">
4
4
  <UiBreadcrumbs
@@ -28,12 +28,12 @@ const items = computed<ILibraryFeedItem[]>(() => getLibraryModelsStub().map(mode
28
28
  to: generateLink(`/models/${convertString().toSlug(model.title)}`),
29
29
  avatarUrl: model.avatarUrl,
30
30
  country: model.country,
31
- tags: model.tags.map(tag => convertString().toChips(tag, 'tag')),
31
+ parameters: [model.guyType, model.bodyBuild, model.ethnicity],
32
+ age: `${model.age} ${t('yo')}`,
32
33
  newCount: model.newCount,
33
34
  meta: [
34
35
  { icon: 'video-camera', text: String(model.videosCount) },
35
- { text: `${model.age} ${t('yo')}` },
36
- { icon: 'date', text: t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }) },
36
+ { icon: 'date', text: t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }), desktopOnly: true },
37
37
  ],
38
38
  videosCount: model.videosCount,
39
39
  videos: model.videos,
@@ -32,7 +32,10 @@ export type LibraryModelStub = LibraryFollowBase & {
32
32
  title: string
33
33
  };
34
34
  age: number;
35
- tags: string[];
35
+ /** Параметры модели из API: guy_type, body_build, ethnicity */
36
+ guyType: string;
37
+ bodyBuild: string;
38
+ ethnicity: string;
36
39
  };
37
40
 
38
41
  export type LibraryChannelStub = LibraryFollowBase & {
@@ -65,21 +68,21 @@ function buildVideos(slug: string, offset: number, newCount: number): LibraryFol
65
68
 
66
69
  /** Порядок по числу новинок намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */
67
70
  const MODELS = [
68
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
69
- { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top', 'Tattooed'], updatedAgo: 2 * DAY, videoOffset: 9 },
70
- { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom', 'Blond', 'Solo'], updatedAgo: 9 * DAY, videoOffset: 0 },
71
- { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom', 'Fisting', 'Hardcore', 'Professional'], updatedAgo: 21 * DAY, videoOffset: 6 },
72
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
73
- { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top', 'Tattooed'], updatedAgo: 2 * DAY, videoOffset: 9 },
74
- { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom', 'Blond', 'Solo'], updatedAgo: 9 * DAY, videoOffset: 0 },
75
- { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom', 'Fisting', 'Hardcore', 'Professional'], updatedAgo: 21 * DAY, videoOffset: 6 },
76
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
77
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
78
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
79
- { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile', 'Smooth', 'Bareback', 'Amateur'], updatedAgo: 6 * HOUR, videoOffset: 3 },
80
- { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top', 'Tattooed'], updatedAgo: 2 * DAY, videoOffset: 9 },
81
- { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom', 'Blond', 'Solo'], updatedAgo: 9 * DAY, videoOffset: 0 },
82
- { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom', 'Fisting', 'Hardcore', 'Professional'], updatedAgo: 21 * DAY, videoOffset: 6 },
71
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 },
72
+ { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, guyType: 'Daddy', bodyBuild: 'Muscular', ethnicity: 'Latino', updatedAgo: 2 * DAY, videoOffset: 9 },
73
+ { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, guyType: 'Twink', bodyBuild: 'Athletic', ethnicity: 'Caucasian', updatedAgo: 9 * DAY, videoOffset: 0 },
74
+ { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, guyType: 'Jock', bodyBuild: 'Muscular', ethnicity: 'Black', updatedAgo: 21 * DAY, videoOffset: 6 },
75
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 },
76
+ { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, guyType: 'Daddy', bodyBuild: 'Muscular', ethnicity: 'Latino', updatedAgo: 2 * DAY, videoOffset: 9 },
77
+ { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, guyType: 'Twink', bodyBuild: 'Athletic', ethnicity: 'Caucasian', updatedAgo: 9 * DAY, videoOffset: 0 },
78
+ { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, guyType: 'Jock', bodyBuild: 'Muscular', ethnicity: 'Black', updatedAgo: 21 * DAY, videoOffset: 6 },
79
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 },
80
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 },
81
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 },
82
+ { title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, guyType: 'Twink', bodyBuild: 'Slim', ethnicity: 'Caucasian', updatedAgo: 6 * HOUR, videoOffset: 3 },
83
+ { title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, guyType: 'Daddy', bodyBuild: 'Muscular', ethnicity: 'Latino', updatedAgo: 2 * DAY, videoOffset: 9 },
84
+ { title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, guyType: 'Twink', bodyBuild: 'Athletic', ethnicity: 'Caucasian', updatedAgo: 9 * DAY, videoOffset: 0 },
85
+ { title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, guyType: 'Jock', bodyBuild: 'Muscular', ethnicity: 'Black', updatedAgo: 21 * DAY, videoOffset: 6 },
83
86
  ];
84
87
 
85
88
  const CHANNELS = [
@@ -5,6 +5,8 @@ import type { IVideoCard } from './video-card';
5
5
  export interface ILibraryFeedMeta {
6
6
  icon?: string
7
7
  text: string
8
+ /** Второстепенный счётчик: на мобиле в строке не помещается и скрывается */
9
+ desktopOnly?: boolean
8
10
  }
9
11
 
10
12
  /**
@@ -24,6 +26,10 @@ export interface ILibraryFeedItem {
24
26
  title: string
25
27
  }
26
28
  tags?: IChipsItem[]
29
+ /** Строка параметров модели вместо тегов: «Jock · Muscular · Latino» */
30
+ parameters?: string[]
31
+ /** Возраст с суффиксом («34 y.o.») — печатается в хвосте строки параметров приглушённым цветом */
32
+ age?: string
27
33
  /** Роликов с прошлого захода: плашка «N new» и порядок в сортировке «сначала новые». 0 — плашки нет */
28
34
  newCount: number
29
35
  meta: ILibraryFeedMeta[]
@@ -18,4 +18,8 @@ export interface IVideoCard {
18
18
  thumbNum?: number;
19
19
  models?: string[];
20
20
  thumbUrls?: IThumbUrls;
21
+ /** Доля просмотренного, 0–100 — рисует полосу прогресса на постере */
22
+ progress?: number;
23
+ /** Unix timestamp (сек.) последнего просмотра */
24
+ watchedAt?: number;
21
25
  }