itube-specs 0.0.892 → 0.0.895

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.
package/pages/library.vue CHANGED
@@ -1,366 +1,6 @@
1
- <!-- Библиотека: «продолжить просмотр», коллекции и подписки. Данные пока заглушка -->
2
1
  <template>
3
- <div class="library _page _container">
4
- <UiBreadcrumbs
5
- :items="breadcrumbs"
6
- class="_breadcrumbs"
7
- />
8
-
9
- <div class="library__header">
10
- <h1 class="library__title _title">{{ t('navigation.library') }}</h1>
11
-
12
- <UiToggle
13
- v-model="saveHistory"
14
- class="library__save"
15
- left
16
- >{{ t('history_text') }}</UiToggle>
17
- </div>
18
-
19
- <section
20
- v-if="continueWatching.length > 0"
21
- class="library__section"
22
- >
23
- <header class="library__section-header">
24
- <h2 class="library__heading">
25
- <span class="library__heading-icon">
26
- <UiIcon
27
- name="history"
28
- size="16"
29
- />
30
- </span>
31
- {{ t('library.continue_watching') }}
32
- </h2>
33
-
34
- <UiLink
35
- :to="generateLink('/history')"
36
- theme="secondary"
37
- >
38
- {{ t('library.view_all_history') }}
39
- <UiIcon
40
- name="arrow-right"
41
- size="16"
42
- />
43
- </UiLink>
44
- </header>
45
-
46
- <div class="library__continue">
47
- <CardHistory
48
- v-for="item in continueWatching"
49
- :key="`continue-${item.guid}`"
50
- :card="item"
51
- loading="eager"
52
- priority
53
- remaining
54
- @share="onShare"
55
- @remove="onRemove"
56
- />
57
- </div>
58
- </section>
59
-
60
- <section class="library__section">
61
- <header class="library__section-header">
62
- <h2 class="library__heading">
63
- <span class="library__heading-icon">
64
- <UiIcon
65
- name="layers"
66
- size="16"
67
- />
68
- </span>
69
- {{ t('library.collections') }}
70
- </h2>
71
- </header>
72
-
73
- <ul class="library__grid">
74
- <li
75
- v-for="item in collections"
76
- :key="item.title"
77
- >
78
- <LibraryItem
79
- :to="item.to"
80
- :icon="item.icon"
81
- :title="item.title"
82
- :subtitle="item.subtitle"
83
- />
84
- </li>
85
- </ul>
86
- </section>
87
-
88
- <section class="library__section">
89
- <header class="library__section-header">
90
- <h2 class="library__heading">
91
- <span class="library__heading-icon">
92
- <UiIcon
93
- name="plus"
94
- size="16"
95
- />
96
- </span>
97
- {{ t('library.following') }}
98
- </h2>
99
-
100
- <UiBtn theme="secondary">
101
- {{ t('library.whats_new', { count: newTotal }) }}
102
- <UiIcon
103
- name="arrow-right"
104
- size="16"
105
- />
106
- </UiBtn>
107
- </header>
108
-
109
- <ul class="library__grid">
110
- <li
111
- v-for="item in following"
112
- :key="item.title"
113
- >
114
- <LibraryItem
115
- :to="item.to"
116
- :icon="item.icon"
117
- :title="item.title"
118
- :subtitle="item.subtitle"
119
- :badge="item.badge"
120
- :badge-note="item.badgeNote"
121
- :badge-theme="item.badgeTheme"
122
- />
123
- </li>
124
- </ul>
125
- </section>
126
-
127
- <ShareMain
128
- v-model="isShareOpen"
129
- :url="shareUrl"
130
- >
131
- <template #card>
132
- <CardVideoMini
133
- v-if="shareCard"
134
- :card="shareCard"
135
- />
136
- </template>
137
- </ShareMain>
138
- </div>
2
+ <NuxtPage />
139
3
  </template>
140
4
 
141
5
  <script setup lang="ts">
142
- import { getHistoryStub } from '../stubs/history';
143
- import type { HistoryStubCard } from '../stubs/history';
144
- import { libraryStub } from '../stubs/library';
145
- import type { IBreadcrumbItem, IVideoCard } from '../types';
146
-
147
- /** Карточка-ссылка библиотеки в разметке страницы. */
148
- type LibraryLink = {
149
- to: string
150
- icon: string
151
- title: string
152
- subtitle?: string
153
- badge?: string
154
- badgeNote?: string
155
- badgeTheme?: 'accent' | 'bordered'
156
- };
157
-
158
- const CONTINUE_LIMIT = 4;
159
-
160
- const { t } = useI18n();
161
-
162
- const { generateLink } = useGenerateLink();
163
-
164
- const saveHistory = ref(true);
165
-
166
- const removedGuids = ref<string[]>([]);
167
-
168
- /** Недосмотренные ролики: прогресс есть, но до конца не дошли. */
169
- const continueWatching = computed(() => getHistoryStub()
170
- .filter(item => item.progress > 0 && item.progress < 100)
171
- .filter(item => !removedGuids.value.includes(item.guid))
172
- .slice(0, CONTINUE_LIMIT));
173
-
174
- const newTotal = computed(() => libraryStub.newModels + libraryStub.newChannels + libraryStub.newPlaylists);
175
-
176
- const collections = computed<LibraryLink[]>(() => [
177
- {
178
- to: generateLink('/favorites'),
179
- icon: 'heart-stroke',
180
- title: t('favorites'),
181
- subtitle: t('plural.videos', libraryStub.favorites),
182
- },
183
- {
184
- to: `${generateLink('/favorites')}?tab=watch-later`,
185
- icon: 'clock',
186
- title: t('watch_later'),
187
- subtitle: t('plural.videos', libraryStub.watchLater),
188
- },
189
- {
190
- to: generateLink('/playlists'),
191
- icon: 'playlist',
192
- title: t('my_playlists'),
193
- subtitle: t('library.playlists_visibility', {
194
- privateCount: libraryStub.privatePlaylists,
195
- publicCount: libraryStub.publicPlaylists,
196
- }),
197
- },
198
- {
199
- to: generateLink('/history'),
200
- icon: 'history',
201
- title: t('history'),
202
- subtitle: t('library.watched_today', { count: libraryStub.watchedToday }),
203
- },
204
- ]);
205
-
206
- const following = computed<LibraryLink[]>(() => [
207
- {
208
- to: generateLink('/models'),
209
- icon: 'user',
210
- title: t('models'),
211
- subtitle: t('plural.models', libraryStub.models),
212
- badge: t('library.new_count', { count: libraryStub.newModels }),
213
- },
214
- {
215
- to: generateLink('/channels'),
216
- icon: 'play-circle',
217
- title: t('channels'),
218
- subtitle: t('plural.channels', libraryStub.channels),
219
- badge: t('library.new_count', { count: libraryStub.newChannels }),
220
- },
221
- {
222
- to: generateLink('/categories'),
223
- icon: 'grid',
224
- title: t('categories'),
225
- subtitle: t('plural.categories', libraryStub.categories),
226
- badge: `+${libraryStub.categoryVideosPerWeek}`,
227
- badgeNote: t('library.this_week'),
228
- badgeTheme: 'bordered',
229
- },
230
- {
231
- to: generateLink('/playlists'),
232
- icon: 'playlist',
233
- title: t('playlists'),
234
- subtitle: t('plural.playlists', libraryStub.playlists),
235
- badge: t('library.new_count', { count: libraryStub.newPlaylists }),
236
- },
237
- ]);
238
-
239
- function onRemove(card: HistoryStubCard) {
240
- removedGuids.value = [...removedGuids.value, card.guid];
241
- }
242
-
243
- const isShareOpen = ref(false);
244
- const shareCard = ref<IVideoCard | null>(null);
245
-
246
- const shareUrl = computed(() => shareCard.value ? generateLink(`/videos/${shareCard.value.id}`) : '');
247
-
248
- function onShare(card: IVideoCard) {
249
- shareCard.value = card;
250
- isShareOpen.value = true;
251
- }
252
-
253
- useNoindex();
254
-
255
- const { brandName } = useAppConfig();
256
-
257
- useHead(() => ({
258
- title: `${t('navigation.library')} — ${brandName}`,
259
- }));
260
-
261
- const breadcrumbs = [
262
- {
263
- name: `${t('navigation.main')}`,
264
- to: '/',
265
- },
266
- {
267
- name: `${t('navigation.library')}`,
268
- },
269
- ] as IBreadcrumbItem[];
270
6
  </script>
271
-
272
- <style lang="scss">
273
- .library__header {
274
- display: flex;
275
- flex-wrap: wrap;
276
- align-items: center;
277
- justify-content: space-between;
278
- column-gap: 16px;
279
- row-gap: 12px;
280
- margin-bottom: 24px;
281
- }
282
-
283
- .library__title._title {
284
- margin-bottom: 0;
285
- }
286
-
287
- .library__save .ui-toggle__text {
288
- @include font-sm;
289
-
290
- color: $page-library-toggle-text;
291
- }
292
-
293
- .library__section {
294
- margin-bottom: 48px;
295
- }
296
-
297
- .library__section-header {
298
- display: flex;
299
- flex-wrap: wrap;
300
- align-items: center;
301
- justify-content: space-between;
302
- column-gap: 16px;
303
- row-gap: 12px;
304
- margin-bottom: 16px;
305
- }
306
-
307
- .library__heading {
308
- @include font-xl;
309
-
310
- display: flex;
311
- align-items: center;
312
- column-gap: 8px;
313
- margin: 0;
314
- font-weight: 600;
315
- color: $page-library-heading;
316
- }
317
-
318
- .library__heading-icon {
319
- display: grid;
320
- place-content: center;
321
- flex-shrink: 0;
322
- width: 24px;
323
- height: 24px;
324
- color: $page-library-heading-icon;
325
- background-color: $page-library-heading-icon-bg;
326
- border-radius: $radius-md;
327
- }
328
-
329
- .library__continue {
330
- display: grid;
331
- gap: 16px;
332
- grid-template-columns: 1fr;
333
-
334
- @include from-br(xm) {
335
- grid-template-columns: repeat(2, 1fr);
336
- }
337
-
338
- @include from-br(sm) {
339
- gap: 24px;
340
- grid-template-columns: repeat(3, 1fr);
341
- }
342
-
343
- @include from-br(md) {
344
- grid-template-columns: repeat(4, 1fr);
345
- }
346
- }
347
-
348
- .library__grid {
349
- display: grid;
350
- gap: 16px;
351
- margin: 0;
352
- padding: 0;
353
- list-style: none;
354
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
355
-
356
- @include from-br(sm) {
357
- gap: 24px;
358
- grid-template-columns: repeat(4, 1fr);
359
- }
360
- }
361
-
362
- /* Карточка тянется на всю высоту ряда — иначе соседи с двухстрочным тайтлом разной высоты. */
363
- .library__grid > li {
364
- display: grid;
365
- }
366
- </style>
package/stubs/history.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { VIDEO_POOL } from './videos';
1
2
  import type { IThumbUrls } from '../types';
2
3
 
3
4
  const HOUR = 3600;
@@ -20,24 +21,26 @@ export type HistoryStubCard = {
20
21
  };
21
22
 
22
23
  /**
23
- * Ролики сняты с главной gay.cool — id, тайтлы, тумбы и каналы настоящие.
24
+ * Состояние просмотра поверх общего пула роликов — индекс в индекс с `VIDEO_POOL`.
24
25
  * `watchedAgo` — смещение в секундах от «сейчас», чтобы даты в вёрстке не устаревали.
25
26
  */
26
- const ITEMS = [
27
- { 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', progress: 57, watchedAgo: 2 * HOUR },
28
- { 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', progress: 74, watchedAgo: 5 * HOUR },
29
- { 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', progress: 91, watchedAgo: 8 * HOUR },
30
- { 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', progress: 48, watchedAgo: DAY + 3 * HOUR },
31
- { 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', progress: 100, watchedAgo: DAY + 7 * HOUR },
32
- { id: '175791825', title: 'Kyle Wilkinson flip-fucks bareback with a smooth skinny twink: deep oral, riding and doggy', duration: 586, thumbUrls: { webp: { '320x180': 'https://nlt05.gay.cool/9/0/f/90fd15f9305b32394f8d383113376c27/thumbs/320x180/7.webp', '480x270': 'https://nlt05.gay.cool/9/0/f/90fd15f9305b32394f8d383113376c27/thumbs/480x270/7.webp' } }, previewUrl: 'https://nlp04.gay.cool/9/0/f/90fd15f9305b32394f8d383113376c27/preview/video.mp4', progress: 23, watchedAgo: DAY + 11 * HOUR },
33
- { id: '374648505', title: 'Devon Lebron barebacks tattooed twink after rimjob in Staxus bedroom', duration: 1800, thumbUrls: { webp: { '320x180': 'https://nlt05.gay.cool/a/2/b/a2b4dce906ce8a4c107a5d559013e555/thumbs/320x180/46.webp', '480x270': 'https://nlt05.gay.cool/a/2/b/a2b4dce906ce8a4c107a5d559013e555/thumbs/480x270/46.webp' } }, previewUrl: 'https://nlp04.gay.cool/a/2/b/a2b4dce906ce8a4c107a5d559013e555/preview/video.mp4', progress: 99, watchedAgo: DAY + 16 * HOUR },
34
- { id: '307011761', title: 'Damien Nichols barebacks a tattooed twink: military amateur, deepthroat to raw anal and cumshot', duration: 419, thumbUrls: { webp: { '320x180': 'https://nlt01.gay.cool/5/a/0/5a00c5dd536d7d7ce4baef5e45930f91/thumbs/320x180/43.webp', '480x270': 'https://nlt01.gay.cool/5/a/0/5a00c5dd536d7d7ce4baef5e45930f91/thumbs/480x270/43.webp' } }, previewUrl: 'https://nlp02.gay.cool/5/a/0/5a00c5dd536d7d7ce4baef5e45930f91/preview/video.mp4', progress: 66, watchedAgo: 2 * DAY + 4 * HOUR },
35
- { 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', progress: 12, watchedAgo: 2 * DAY + 9 * HOUR },
36
- { 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', progress: 88, watchedAgo: 2 * DAY + 15 * HOUR },
37
- { 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', progress: 35, watchedAgo: 4 * DAY + 6 * HOUR },
38
- { 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', progress: 71, watchedAgo: 4 * DAY + 12 * HOUR },
27
+ const WATCH_STATE = [
28
+ { progress: 57, watchedAgo: 2 * HOUR },
29
+ { progress: 74, watchedAgo: 5 * HOUR },
30
+ { progress: 91, watchedAgo: 8 * HOUR },
31
+ { progress: 48, watchedAgo: DAY + 3 * HOUR },
32
+ { progress: 100, watchedAgo: DAY + 7 * HOUR },
33
+ { progress: 23, watchedAgo: DAY + 11 * HOUR },
34
+ { progress: 99, watchedAgo: DAY + 16 * HOUR },
35
+ { progress: 66, watchedAgo: 2 * DAY + 4 * HOUR },
36
+ { progress: 12, watchedAgo: 2 * DAY + 9 * HOUR },
37
+ { progress: 88, watchedAgo: 2 * DAY + 15 * HOUR },
38
+ { progress: 35, watchedAgo: 4 * DAY + 6 * HOUR },
39
+ { progress: 71, watchedAgo: 4 * DAY + 12 * HOUR },
39
40
  ];
40
41
 
42
+ const ITEMS = VIDEO_POOL.map((video, index) => ({ ...video, ...WATCH_STATE[index] }));
43
+
41
44
  /** Тот же набор, отодвинутый глубже в прошлое — чтобы набралось несколько страниц. */
42
45
  const WAVES = [0, 7 * DAY, 21 * DAY, 45 * DAY, 120 * DAY];
43
46
 
@@ -0,0 +1,135 @@
1
+ import { VIDEO_POOL } from './videos';
2
+ import type { StubVideo } from './videos';
3
+
4
+ const HOUR = 3600;
5
+ const DAY = 24 * HOUR;
6
+
7
+ /** Сколько последних роликов сущности показываем в ленте. */
8
+ const PREVIEW_LIMIT = 5;
9
+
10
+ export type LibraryFollowVideo = StubVideo & {
11
+ guid: string;
12
+ isNew: boolean;
13
+ };
14
+
15
+ type LibraryFollowBase = {
16
+ guid: string;
17
+ title: string;
18
+ videosCount: number;
19
+ /** Сколько роликов вышло с прошлого захода; 0 — плашки «новое» нет */
20
+ newCount: number;
21
+ /** Unix timestamp (сек.) последнего добавленного ролика */
22
+ lastVideoAddedAt: number;
23
+ videos: LibraryFollowVideo[];
24
+ };
25
+
26
+ export type LibraryModelStub = LibraryFollowBase & {
27
+ avatarUrl: string;
28
+ country: {
29
+ name: string
30
+ title: string
31
+ };
32
+ age: number;
33
+ tags: string[];
34
+ };
35
+
36
+ export type LibraryChannelStub = LibraryFollowBase & {
37
+ avatarUrl?: string;
38
+ followersCount: number;
39
+ tags: string[];
40
+ };
41
+
42
+ export type LibraryCategoryStub = LibraryFollowBase;
43
+
44
+ const now = () => Math.floor(Date.now() / 1000);
45
+
46
+ const toSlug = (title: string) => title.toLowerCase().replace(/\s+/g, '-');
47
+
48
+ /** Нарезает пул на ленту сущности: guid уникален внутри неё, первые `newCount` помечены новинками. */
49
+ function buildVideos(slug: string, offset: number, newCount: number): LibraryFollowVideo[] {
50
+ return Array.from({ length: PREVIEW_LIMIT }, (_, index) => {
51
+ const video = VIDEO_POOL[(offset + index) % VIDEO_POOL.length];
52
+
53
+ return {
54
+ ...video,
55
+ guid: `${slug}-${video.id}`,
56
+ isNew: index < newCount,
57
+ };
58
+ });
59
+ }
60
+
61
+ /** Порядок по числу новинок намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */
62
+ const MODELS = [
63
+ { 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 },
64
+ { 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 },
65
+ { 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 },
66
+ { 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 },
67
+ ];
68
+
69
+ const CHANNELS = [
70
+ { title: 'Top To Bottom', avatarUrl: undefined, videosCount: 17, followersCount: 12800, newCount: 3, tags: ['Versatile', 'Flip-Flop', 'HD', 'Professional', 'Exclusive', 'Bareback'], updatedAgo: 4 * HOUR, videoOffset: 2 },
71
+ { title: 'Icon Male', avatarUrl: 'https://nli01.joystorage.com/c/0/2/c02a3f8ef14943b5f3da537aaed46fcc.jpeg', videosCount: 128, followersCount: 128400, newCount: 2, tags: ['Muscle', 'Daddy', 'Bareback', 'Professional', 'Exclusive', 'HD', 'Studio'], updatedAgo: DAY, videoOffset: 0 },
72
+ { title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
73
+ { title: 'Johnny Rapid', avatarUrl: 'https://nli01.joystorage.com/e/6/d/e6d0abd734a1a2eaef0baf7ceccfdb5c.jpeg', videosCount: 86, followersCount: 94200, newCount: 0, tags: ['Twink', 'Bareback', 'Group', 'Amateur'], updatedAgo: 14 * DAY, videoOffset: 4 },
74
+ ];
75
+
76
+ /**
77
+ * Широкие категории пополняются постоянно, поэтому новинки у них есть всегда; узкие вроде Firemen
78
+ * неделями стоят на месте — там `newCount: 0`, и лента откатывается на лучшее за неделю.
79
+ */
80
+ const CATEGORIES = [
81
+ { title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
82
+ { title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
83
+ { title: 'Bears', videosCount: 92, newCount: 0, updatedAgo: 11 * DAY, videoOffset: 10 },
84
+ { title: 'Firemen', videosCount: 61, newCount: 0, updatedAgo: 24 * DAY, videoOffset: 8 },
85
+ ];
86
+
87
+ /**
88
+ * Подписки пользователя. Ручек под них пока нет — вёрстка живёт на заглушке.
89
+ * Возраст ролика считаем от «сейчас», чтобы даты в разметке не устаревали.
90
+ */
91
+ export function getLibraryModelsStub(): LibraryModelStub[] {
92
+ const stamp = now();
93
+
94
+ return MODELS.map(({ image, updatedAgo, videoOffset, ...model }) => {
95
+ const guid = toSlug(model.title);
96
+
97
+ return {
98
+ ...model,
99
+ guid,
100
+ avatarUrl: `/img/models/${image}`,
101
+ lastVideoAddedAt: stamp - updatedAgo,
102
+ videos: buildVideos(guid, videoOffset, model.newCount),
103
+ };
104
+ });
105
+ }
106
+
107
+ export function getLibraryChannelsStub(): LibraryChannelStub[] {
108
+ const stamp = now();
109
+
110
+ return CHANNELS.map(({ updatedAgo, videoOffset, ...channel }) => {
111
+ const guid = toSlug(channel.title);
112
+
113
+ return {
114
+ ...channel,
115
+ guid,
116
+ lastVideoAddedAt: stamp - updatedAgo,
117
+ videos: buildVideos(guid, videoOffset, channel.newCount),
118
+ };
119
+ });
120
+ }
121
+
122
+ export function getLibraryCategoriesStub(): LibraryCategoryStub[] {
123
+ const stamp = now();
124
+
125
+ return CATEGORIES.map(({ updatedAgo, videoOffset, ...category }) => {
126
+ const guid = toSlug(category.title);
127
+
128
+ return {
129
+ ...category,
130
+ guid,
131
+ lastVideoAddedAt: stamp - updatedAgo,
132
+ videos: buildVideos(guid, videoOffset, category.newCount),
133
+ };
134
+ });
135
+ }
@@ -0,0 +1,29 @@
1
+ import type { IThumbUrls } from '../types';
2
+
3
+ /** Базовый ролик заглушек: id, тайтлы и тумбы сняты с главной gay.cool. */
4
+ export type StubVideo = {
5
+ id: string;
6
+ title: string;
7
+ duration: number;
8
+ quality?: string;
9
+ thumbUrls: IThumbUrls;
10
+ previewUrl: string;
11
+ channelName?: string;
12
+ channelAvatar?: string;
13
+ };
14
+
15
+ /** Общий пул роликов для всех заглушек — порядок фиксирован, на него завязаны наборы-надстройки. */
16
+ export const VIDEO_POOL: StubVideo[] = [
17
+ { 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' },
19
+ { 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
+ { 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
+ { 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' },
22
+ { id: '175791825', title: 'Kyle Wilkinson flip-fucks bareback with a smooth skinny twink: deep oral, riding and doggy', duration: 586, thumbUrls: { webp: { '320x180': 'https://nlt05.gay.cool/9/0/f/90fd15f9305b32394f8d383113376c27/thumbs/320x180/7.webp', '480x270': 'https://nlt05.gay.cool/9/0/f/90fd15f9305b32394f8d383113376c27/thumbs/480x270/7.webp' } }, previewUrl: 'https://nlp04.gay.cool/9/0/f/90fd15f9305b32394f8d383113376c27/preview/video.mp4' },
23
+ { id: '374648505', title: 'Devon Lebron barebacks tattooed twink after rimjob in Staxus bedroom', duration: 1800, thumbUrls: { webp: { '320x180': 'https://nlt05.gay.cool/a/2/b/a2b4dce906ce8a4c107a5d559013e555/thumbs/320x180/46.webp', '480x270': 'https://nlt05.gay.cool/a/2/b/a2b4dce906ce8a4c107a5d559013e555/thumbs/480x270/46.webp' } }, previewUrl: 'https://nlp04.gay.cool/a/2/b/a2b4dce906ce8a4c107a5d559013e555/preview/video.mp4' },
24
+ { id: '307011761', title: 'Damien Nichols barebacks a tattooed twink: military amateur, deepthroat to raw anal and cumshot', duration: 419, thumbUrls: { webp: { '320x180': 'https://nlt01.gay.cool/5/a/0/5a00c5dd536d7d7ce4baef5e45930f91/thumbs/320x180/43.webp', '480x270': 'https://nlt01.gay.cool/5/a/0/5a00c5dd536d7d7ce4baef5e45930f91/thumbs/480x270/43.webp' } }, previewUrl: 'https://nlp02.gay.cool/5/a/0/5a00c5dd536d7d7ce4baef5e45930f91/preview/video.mp4' },
25
+ { 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
+ { 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
+ { 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' },
29
+ ];
package/types/index.d.ts CHANGED
@@ -49,6 +49,7 @@ export * from './navigation-items.d.ts';
49
49
  export * from './link-item.d.ts';
50
50
  export * from './fluid-player.d.ts';
51
51
  export * from './chips-item.d.ts';
52
+ export * from './library-feed.d.ts';
52
53
  export * from './nav-link-item.d.ts';
53
54
  export * from './paginated-response.d.ts';
54
55
  export * from './request-pagination.d.ts';
@@ -0,0 +1,34 @@
1
+ import type { IChipsItem } from './chips-item';
2
+ import type { IVideoCard } from './video-card';
3
+
4
+ /** Счётчик в строке под тегами: «17», «128.4K followers», «Updated 04.06.2026». */
5
+ export interface ILibraryFeedMeta {
6
+ icon?: string
7
+ text: string
8
+ }
9
+
10
+ /**
11
+ * Блок одной подписки в «Библиотеке». Модели, каналы и категории отличаются только
12
+ * составом шапки, поэтому страница приводит их к этой форме, а вёрстка одна.
13
+ */
14
+ export interface ILibraryFeedItem {
15
+ guid: string
16
+ title: string
17
+ /** Ссылка на саму сущность */
18
+ to: string
19
+ /** Нет — вместо картинки рисуются инициалы */
20
+ avatarUrl?: string
21
+ /** Только у моделей */
22
+ country?: {
23
+ name: string
24
+ title: string
25
+ }
26
+ tags?: IChipsItem[]
27
+ /** Роликов с прошлого захода: плашка «N new» и порядок в сортировке «сначала новые». 0 — плашки нет */
28
+ newCount: number
29
+ meta: ILibraryFeedMeta[]
30
+ videosCount: number
31
+ /** Подпись над лентой: «Latest» / «Top this week». Нет — лента без подписи */
32
+ feedTitle?: string
33
+ videos: IVideoCard[]
34
+ }