itube-specs 0.0.897 → 0.0.899
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/assets/icons/common/bell.svg +1 -0
- package/assets/scss/mixins/_font-sizes.scss +5 -0
- package/assets/scss/themes/dark/_theme.scss +38 -26
- package/assets/scss/themes/light/_theme.scss +34 -24
- package/assets/scss/vars/_palette.scss +1 -0
- package/assets/scss/vars/_tokens.scss +2 -0
- package/components/card/category.vue +22 -1
- package/components/card/playlist.vue +32 -4
- package/components/card/round.vue +48 -4
- package/components/filter/main.vue +2 -6
- package/components/filter/model.vue +2 -6
- package/components/grid/playlists.vue +2 -0
- package/components/layout/burger-item.vue +8 -8
- package/components/layout/extra-links.vue +2 -0
- package/components/layout/header.vue +29 -3
- package/components/layout/logo.vue +11 -2
- package/components/layout/notifications.vue +79 -0
- package/components/section/swiper-cards.vue +68 -34
- package/components/ui/btn.vue +15 -0
- package/components/ui/popup.vue +0 -4
- package/components/ui/section-title.vue +21 -6
- package/components/ui/swiper.vue +19 -4
- package/components/ui/view-all.vue +113 -0
- package/lib/scheme/profile-navigation-items.ts +0 -27
- package/package.json +1 -1
- package/pages/following.vue +205 -0
- package/pages/library/index.vue +24 -52
- package/pages/search/[slug]/index.vue +19 -37
- package/stubs/library-follows.ts +75 -2
- package/types/button-themes.d.ts +1 -1
- package/types/category-card.d.ts +2 -0
- package/types/playlist-card.d.ts +2 -0
- package/types/round-card.d.ts +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<!-- Подписки: что нового у моделей, каналов, категорий и плейлистов. Данные пока заглушка -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="following _page _container">
|
|
4
|
+
<UiBreadcrumbs
|
|
5
|
+
:items="breadcrumbs"
|
|
6
|
+
class="_breadcrumbs"
|
|
7
|
+
/>
|
|
8
|
+
|
|
9
|
+
<h1 class="_title">{{ t('following.title') }}</h1>
|
|
10
|
+
|
|
11
|
+
<SectionSwiperCards
|
|
12
|
+
:items="models"
|
|
13
|
+
:count="t('following.updates', { count: modelsUpdates })"
|
|
14
|
+
:title="t('models')"
|
|
15
|
+
:all-link-text="t('following.view_all')"
|
|
16
|
+
:all-link-note="t('plural.models', models.length)"
|
|
17
|
+
:header-link-text="t('following.all_models')"
|
|
18
|
+
all-link="/library/models"
|
|
19
|
+
icon="user"
|
|
20
|
+
size="xl"
|
|
21
|
+
placeholder-avatar="/img/model-placeholder.webp"
|
|
22
|
+
class="following__section"
|
|
23
|
+
without-count
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<SectionSwiperCards
|
|
27
|
+
:items="channels"
|
|
28
|
+
:count="t('following.updates', { count: channelsUpdates })"
|
|
29
|
+
:title="t('channels')"
|
|
30
|
+
:all-link-text="t('following.view_all')"
|
|
31
|
+
:all-link-note="t('plural.channels', channels.length)"
|
|
32
|
+
:header-link-text="t('following.all_channels')"
|
|
33
|
+
all-link="/library/channels"
|
|
34
|
+
icon="play-circle"
|
|
35
|
+
size="xl"
|
|
36
|
+
placeholder-avatar="/img/logo-placeholder.svg"
|
|
37
|
+
class="following__section"
|
|
38
|
+
without-count
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<section class="following__section">
|
|
42
|
+
<header class="following__header">
|
|
43
|
+
<UiSectionTitle
|
|
44
|
+
:title="t('categories')"
|
|
45
|
+
:count="t('following.updates', { count: categoriesUpdates })"
|
|
46
|
+
icon="grid"
|
|
47
|
+
title-tag="h2"
|
|
48
|
+
size="xl"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<!-- На мобиле плитки «все» в гриде нет, поэтому ссылка вынесена в шапку -->
|
|
52
|
+
<UiLink
|
|
53
|
+
:to="generateLink('/library/categories')"
|
|
54
|
+
class="following__link _to-sm"
|
|
55
|
+
theme="simple"
|
|
56
|
+
>
|
|
57
|
+
{{ t('following.all_categories') }}
|
|
58
|
+
<UiIcon
|
|
59
|
+
name="arrow-right"
|
|
60
|
+
size="16"
|
|
61
|
+
/>
|
|
62
|
+
</UiLink>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
<GridCategories :categories="categories">
|
|
66
|
+
<UiViewAll
|
|
67
|
+
:text="t('following.view_all')"
|
|
68
|
+
:note="t('plural.categories', categories.length)"
|
|
69
|
+
class="_from-sm"
|
|
70
|
+
to="/library/categories"
|
|
71
|
+
/>
|
|
72
|
+
</GridCategories>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
75
|
+
<section class="following__section">
|
|
76
|
+
<header class="following__header">
|
|
77
|
+
<UiSectionTitle
|
|
78
|
+
:title="t('playlists')"
|
|
79
|
+
:count="t('following.updates', { count: playlistsUpdates })"
|
|
80
|
+
icon="playlist"
|
|
81
|
+
title-tag="h2"
|
|
82
|
+
size="xl"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<UiLink
|
|
86
|
+
:to="generateLink('/playlists')"
|
|
87
|
+
class="following__link _to-sm"
|
|
88
|
+
theme="simple"
|
|
89
|
+
>
|
|
90
|
+
{{ t('following.all_playlists') }}
|
|
91
|
+
<UiIcon
|
|
92
|
+
name="arrow-right"
|
|
93
|
+
size="16"
|
|
94
|
+
/>
|
|
95
|
+
</UiLink>
|
|
96
|
+
</header>
|
|
97
|
+
|
|
98
|
+
<GridPlaylists :items="playlists">
|
|
99
|
+
<UiViewAll
|
|
100
|
+
:text="t('following.view_all')"
|
|
101
|
+
:note="t('plural.playlists', playlists.length)"
|
|
102
|
+
class="_from-sm"
|
|
103
|
+
to="/playlists"
|
|
104
|
+
/>
|
|
105
|
+
</GridPlaylists>
|
|
106
|
+
</section>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<script setup lang="ts">
|
|
111
|
+
import {
|
|
112
|
+
getLibraryCategoriesStub,
|
|
113
|
+
getLibraryChannelsStub,
|
|
114
|
+
getLibraryModelsStub,
|
|
115
|
+
getLibraryPlaylistsStub
|
|
116
|
+
} from '../stubs/library-follows';
|
|
117
|
+
import { convertString } from '../runtime';
|
|
118
|
+
import type { IBreadcrumbItem, ICategoryCard, IRoundCard } from '../types';
|
|
119
|
+
|
|
120
|
+
const { t } = useI18n();
|
|
121
|
+
const { generateLink } = useGenerateLink();
|
|
122
|
+
|
|
123
|
+
/** Сначала те, у кого больше новинок — ради этого страница и открывается. */
|
|
124
|
+
const byNewCount = <T extends { newCount: number }>(items: T[]) => [...items].sort((a, b) => b.newCount - a.newCount);
|
|
125
|
+
|
|
126
|
+
const sumUpdates = (items: { newCount: number }[]) => items.reduce((total, item) => total + item.newCount, 0);
|
|
127
|
+
|
|
128
|
+
const modelsStub = computed(() => byNewCount(getLibraryModelsStub()));
|
|
129
|
+
const channelsStub = computed(() => byNewCount(getLibraryChannelsStub()));
|
|
130
|
+
const categoriesStub = computed(() => byNewCount(getLibraryCategoriesStub()));
|
|
131
|
+
const playlists = computed(() => byNewCount(getLibraryPlaylistsStub()));
|
|
132
|
+
|
|
133
|
+
const models = computed<IRoundCard[]>(() => modelsStub.value.map(model => ({
|
|
134
|
+
title: model.title,
|
|
135
|
+
primaryImageUrl: model.avatarUrl,
|
|
136
|
+
videosCount: model.videosCount,
|
|
137
|
+
link: `/models/${convertString().toSlug(model.title)}`,
|
|
138
|
+
newCount: model.newCount,
|
|
139
|
+
})));
|
|
140
|
+
|
|
141
|
+
const channels = computed<IRoundCard[]>(() => channelsStub.value.map(channel => ({
|
|
142
|
+
title: channel.title,
|
|
143
|
+
primaryImageUrl: channel.avatarUrl ?? '',
|
|
144
|
+
videosCount: channel.videosCount,
|
|
145
|
+
link: `/channels/${convertString().toSlug(channel.title)}`,
|
|
146
|
+
newCount: channel.newCount,
|
|
147
|
+
})));
|
|
148
|
+
|
|
149
|
+
const categories = computed<ICategoryCard[]>(() => categoriesStub.value.map(category => ({
|
|
150
|
+
guid: category.guid,
|
|
151
|
+
name: convertString().toSlug(category.title),
|
|
152
|
+
title: category.title,
|
|
153
|
+
videosCount: category.videosCount,
|
|
154
|
+
thumbUrls: category.videos[0]?.thumbUrls,
|
|
155
|
+
newCount: category.newCount,
|
|
156
|
+
})));
|
|
157
|
+
|
|
158
|
+
const modelsUpdates = computed(() => sumUpdates(modelsStub.value));
|
|
159
|
+
const channelsUpdates = computed(() => sumUpdates(channelsStub.value));
|
|
160
|
+
const categoriesUpdates = computed(() => sumUpdates(categoriesStub.value));
|
|
161
|
+
const playlistsUpdates = computed(() => sumUpdates(playlists.value));
|
|
162
|
+
|
|
163
|
+
useNoindex();
|
|
164
|
+
|
|
165
|
+
const { brandName } = useAppConfig();
|
|
166
|
+
|
|
167
|
+
useHead(() => ({
|
|
168
|
+
title: `${t('library.following')} — ${brandName}`,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const breadcrumbs = [
|
|
172
|
+
{
|
|
173
|
+
name: `${t('navigation.main')}`,
|
|
174
|
+
to: '/',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: `${t('library.following')}`,
|
|
178
|
+
},
|
|
179
|
+
] as IBreadcrumbItem[];
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<style lang="scss">
|
|
183
|
+
.following__section {
|
|
184
|
+
margin-bottom: 48px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.following__header {
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
column-gap: 16px;
|
|
192
|
+
margin-bottom: 16px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.following__link {
|
|
196
|
+
flex-shrink: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Плитка «смотреть все» часто оказывается одна в новом ряду: без равных рядов
|
|
200
|
+
ряд сжимается по её контенту и она выходит ниже карточек. */
|
|
201
|
+
.following .grid-categories,
|
|
202
|
+
.following .grid-playlists {
|
|
203
|
+
grid-auto-rows: 1fr;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
package/pages/library/index.vue
CHANGED
|
@@ -21,15 +21,12 @@
|
|
|
21
21
|
class="library__section"
|
|
22
22
|
>
|
|
23
23
|
<header class="library__section-header">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</span>
|
|
31
|
-
{{ t('library.continue_watching') }}
|
|
32
|
-
</h2>
|
|
24
|
+
<UiSectionTitle
|
|
25
|
+
:title="t('library.continue_watching')"
|
|
26
|
+
icon="history"
|
|
27
|
+
title-tag="h2"
|
|
28
|
+
size="xl"
|
|
29
|
+
/>
|
|
33
30
|
|
|
34
31
|
<UiLink
|
|
35
32
|
:to="generateLink('/history')"
|
|
@@ -59,15 +56,12 @@
|
|
|
59
56
|
|
|
60
57
|
<section class="library__section">
|
|
61
58
|
<header class="library__section-header">
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
</span>
|
|
69
|
-
{{ t('library.collections') }}
|
|
70
|
-
</h2>
|
|
59
|
+
<UiSectionTitle
|
|
60
|
+
:title="t('library.collections')"
|
|
61
|
+
icon="layers"
|
|
62
|
+
title-tag="h2"
|
|
63
|
+
size="xl"
|
|
64
|
+
/>
|
|
71
65
|
</header>
|
|
72
66
|
|
|
73
67
|
<ul class="library__grid">
|
|
@@ -87,23 +81,23 @@
|
|
|
87
81
|
|
|
88
82
|
<section class="library__section">
|
|
89
83
|
<header class="library__section-header">
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
84
|
+
<UiSectionTitle
|
|
85
|
+
:title="t('library.following')"
|
|
86
|
+
icon="plus"
|
|
87
|
+
title-tag="h2"
|
|
88
|
+
size="xl"
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<UiLink
|
|
92
|
+
:to="generateLink('/following')"
|
|
93
|
+
theme="secondary"
|
|
94
|
+
>
|
|
101
95
|
{{ t('library.whats_new', { count: newTotal }) }}
|
|
102
96
|
<UiIcon
|
|
103
97
|
name="arrow-right"
|
|
104
98
|
size="16"
|
|
105
99
|
/>
|
|
106
|
-
</
|
|
100
|
+
</UiLink>
|
|
107
101
|
</header>
|
|
108
102
|
|
|
109
103
|
<ul class="library__grid">
|
|
@@ -304,28 +298,6 @@ const breadcrumbs = [
|
|
|
304
298
|
margin-bottom: 16px;
|
|
305
299
|
}
|
|
306
300
|
|
|
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
301
|
.library__continue {
|
|
330
302
|
display: grid;
|
|
331
303
|
gap: 16px;
|
|
@@ -56,13 +56,12 @@
|
|
|
56
56
|
v-if="modelsData.total > MAXIMUM_MODELS"
|
|
57
57
|
#append
|
|
58
58
|
>
|
|
59
|
-
<
|
|
60
|
-
:to="
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</NuxtLink>
|
|
59
|
+
<UiViewAll
|
|
60
|
+
:to="`/search/${slug}/models`"
|
|
61
|
+
:text="`+${modelsData.total - MAXIMUM_MODELS}`"
|
|
62
|
+
:note="$t('models')"
|
|
63
|
+
mini
|
|
64
|
+
/>
|
|
66
65
|
</template>
|
|
67
66
|
</UiSwiper>
|
|
68
67
|
</div>
|
|
@@ -86,14 +85,13 @@
|
|
|
86
85
|
v-if="featureFlags.AdsNativeEnabled"
|
|
87
86
|
hydrate-on-visible
|
|
88
87
|
/>
|
|
89
|
-
<
|
|
88
|
+
<UiViewAll
|
|
90
89
|
v-if="videosData.items.length > MAXIMUM_VIDEOS || videosData.total > MAXIMUM_VIDEOS"
|
|
91
|
-
:to="
|
|
90
|
+
:to="`/search/${slug}/videos`"
|
|
91
|
+
:text="`+${videosData.total - MAXIMUM_VIDEOS}`"
|
|
92
|
+
:note="$t('show_more_videos')"
|
|
92
93
|
class="search__show-all"
|
|
93
|
-
|
|
94
|
-
<span class="search__show-all-number">+{{ videosData.total - MAXIMUM_VIDEOS }}</span>
|
|
95
|
-
{{ $t('show_more_videos') }}
|
|
96
|
-
</NuxtLink>
|
|
94
|
+
/>
|
|
97
95
|
</template>
|
|
98
96
|
</GridVideos>
|
|
99
97
|
</div>
|
|
@@ -232,38 +230,22 @@ const breadcrumbs = [
|
|
|
232
230
|
min-width: 0;
|
|
233
231
|
}
|
|
234
232
|
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
/* Слайд ровно по круглой карточке: 80px на мобиле, 96px с sm */
|
|
234
|
+
.search__result-section .ui-swiper__slide {
|
|
235
|
+
width: var(--card-round-size);
|
|
236
|
+
}
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
place-content: center;
|
|
238
|
+
/* Плитка стоит в гриде видео — равняем её по превью, а не по всей карточке */
|
|
239
|
+
.search__show-all {
|
|
241
240
|
align-self: start;
|
|
242
241
|
aspect-ratio: 16/9;
|
|
243
|
-
background-color: $search-show-all-bg;
|
|
244
|
-
border-radius: $radius-lg;
|
|
245
|
-
border: 1px solid $search-show-all-border;
|
|
246
|
-
color: $search-show-all-text;
|
|
247
|
-
|
|
248
|
-
@include hover(border-color, color) {
|
|
249
|
-
border-color: $search-show-all-border--hover;
|
|
250
|
-
color: $search-show-all-text--hover;
|
|
251
|
-
}
|
|
252
242
|
}
|
|
253
243
|
|
|
254
|
-
|
|
244
|
+
/* Здесь главный акцент — число, поэтому крупнее дефолтной подписи плитки */
|
|
245
|
+
.search__show-all .ui-view-all__text {
|
|
255
246
|
@include font-2xl;
|
|
256
247
|
|
|
257
|
-
text-align: center;
|
|
258
248
|
font-weight: 700;
|
|
259
|
-
color: $search-show-all-number;
|
|
260
|
-
transition: var(--transition) color;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.search__show-all:is(:hover,:focus-visible) .search__show-all-number {
|
|
264
|
-
@include from-br(sm) {
|
|
265
|
-
color: $search-show-all-number--hover;
|
|
266
|
-
}
|
|
267
249
|
}
|
|
268
250
|
|
|
269
251
|
.search__title {
|
package/stubs/library-follows.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { VIDEO_POOL } from './videos';
|
|
2
2
|
import type { StubVideo } from './videos';
|
|
3
|
+
import { PlaylistType } from '../runtime/constants/playlist-type';
|
|
4
|
+
import type { IPlaylistCard } from '../types';
|
|
3
5
|
|
|
4
6
|
const HOUR = 3600;
|
|
5
7
|
const DAY = 24 * HOUR;
|
|
@@ -41,6 +43,9 @@ export type LibraryChannelStub = LibraryFollowBase & {
|
|
|
41
43
|
|
|
42
44
|
export type LibraryCategoryStub = LibraryFollowBase;
|
|
43
45
|
|
|
46
|
+
/** Плейлист подписки: карточка как в общем гриде + число новых роликов. */
|
|
47
|
+
export type LibraryPlaylistStub = IPlaylistCard & { newCount: number };
|
|
48
|
+
|
|
44
49
|
const now = () => Math.floor(Date.now() / 1000);
|
|
45
50
|
|
|
46
51
|
const toSlug = (title: string) => title.toLowerCase().replace(/\s+/g, '-');
|
|
@@ -64,6 +69,17 @@ const MODELS = [
|
|
|
64
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 },
|
|
65
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 },
|
|
66
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 },
|
|
67
83
|
];
|
|
68
84
|
|
|
69
85
|
const CHANNELS = [
|
|
@@ -71,6 +87,18 @@ const CHANNELS = [
|
|
|
71
87
|
{ 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
88
|
{ title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
|
|
73
89
|
{ 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 },
|
|
90
|
+
{ 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 },
|
|
91
|
+
{ 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 },
|
|
92
|
+
{ title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
|
|
93
|
+
{ 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 },
|
|
94
|
+
{ 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 },
|
|
95
|
+
{ 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 },
|
|
96
|
+
{ 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 },
|
|
97
|
+
{ 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 },
|
|
98
|
+
{ 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 },
|
|
99
|
+
{ 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 },
|
|
100
|
+
{ title: 'Sean Cody', avatarUrl: undefined, videosCount: 64, followersCount: 210500, newCount: 1, tags: ['Jock', 'Muscle', 'Bareback', 'Studio'], updatedAgo: 5 * DAY, videoOffset: 7 },
|
|
101
|
+
{ 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
102
|
];
|
|
75
103
|
|
|
76
104
|
/**
|
|
@@ -80,8 +108,27 @@ const CHANNELS = [
|
|
|
80
108
|
const CATEGORIES = [
|
|
81
109
|
{ title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
|
|
82
110
|
{ title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
|
|
83
|
-
{ title: '
|
|
84
|
-
{ title: '
|
|
111
|
+
{ title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
|
|
112
|
+
{ title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
|
|
113
|
+
{ title: 'Bareback', videosCount: 214, newCount: 3, updatedAgo: 3 * HOUR, videoOffset: 1 },
|
|
114
|
+
{ title: 'Twinks', videosCount: 176, newCount: 2, updatedAgo: 9 * HOUR, videoOffset: 5 },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/** Чужие плейлисты, на которые подписан пользователь. */
|
|
118
|
+
const PLAYLISTS = [
|
|
119
|
+
{ name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 4, createdAgo: 12 * DAY, videoOffset: 0 },
|
|
120
|
+
{ name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 5, createdAgo: 28 * DAY, videoOffset: 4 },
|
|
121
|
+
{ name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
|
|
122
|
+
{ name: 'Late night picks', username: 'romerofx', videosCount: 7, likes: 42, views: 340, newCount: 1, createdAgo: 5 * DAY, videoOffset: 2 },
|
|
123
|
+
{ name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 4, createdAgo: 12 * DAY, videoOffset: 0 },
|
|
124
|
+
{ name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 5, createdAgo: 28 * DAY, videoOffset: 4 },
|
|
125
|
+
{ name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
|
|
126
|
+
{ name: 'Late night picks', username: 'romerofx', videosCount: 7, likes: 42, views: 340, newCount: 1, createdAgo: 5 * DAY, videoOffset: 2 },
|
|
127
|
+
{ name: 'Suits after office', username: 'andyblair2', videosCount: 24, likes: 156, views: 1200, newCount: 4, createdAgo: 12 * DAY, videoOffset: 0 },
|
|
128
|
+
{ name: 'Weekend binge', username: 'mattdvoe', videosCount: 33, likes: 271, views: 2400, newCount: 5, createdAgo: 28 * DAY, videoOffset: 4 },
|
|
129
|
+
{ name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
|
|
130
|
+
{ name: 'Morning workout', username: 'andyblair2', videosCount: 12, likes: 98, views: 820, newCount: 2, createdAgo: 96 * DAY, videoOffset: 8 },
|
|
131
|
+
{ name: 'Late night picks', username: 'romerofx', videosCount: 7, likes: 42, views: 340, newCount: 1, createdAgo: 5 * DAY, videoOffset: 2 },
|
|
85
132
|
];
|
|
86
133
|
|
|
87
134
|
/**
|
|
@@ -119,6 +166,32 @@ export function getLibraryChannelsStub(): LibraryChannelStub[] {
|
|
|
119
166
|
});
|
|
120
167
|
}
|
|
121
168
|
|
|
169
|
+
/** Мозаика обложек плейлиста: карточка раскладывает до 4 тумб. */
|
|
170
|
+
function buildThumbs(offset: number, count: number) {
|
|
171
|
+
return Array.from({ length: Math.min(count, 4) }, (_, index) => (
|
|
172
|
+
VIDEO_POOL[(offset + index) % VIDEO_POOL.length].thumbUrls
|
|
173
|
+
));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getLibraryPlaylistsStub(): LibraryPlaylistStub[] {
|
|
177
|
+
const stamp = now();
|
|
178
|
+
|
|
179
|
+
return PLAYLISTS.map(({ createdAgo, videoOffset, ...playlist }, index) => ({
|
|
180
|
+
...playlist,
|
|
181
|
+
id: `${index + 1}`,
|
|
182
|
+
playlistType: PlaylistType.Public,
|
|
183
|
+
created: stamp - createdAgo,
|
|
184
|
+
firstVideoId: VIDEO_POOL[videoOffset % VIDEO_POOL.length].id,
|
|
185
|
+
thumbUrls: buildThumbs(videoOffset, playlist.videosCount),
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Сумма новинок по всем подпискам — счётчик на колокольчике и в шапках секций. */
|
|
190
|
+
export function getFollowingUpdatesTotal(): number {
|
|
191
|
+
return [...MODELS, ...CHANNELS, ...CATEGORIES, ...PLAYLISTS]
|
|
192
|
+
.reduce((total, item) => total + item.newCount, 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
122
195
|
export function getLibraryCategoriesStub(): LibraryCategoryStub[] {
|
|
123
196
|
const stamp = now();
|
|
124
197
|
|
package/types/button-themes.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type ButtonThemes = 'primary' | 'secondary' | 'warning' | 'muted' | undefined
|
|
1
|
+
export type ButtonThemes = 'primary' | 'secondary' | 'warning' | 'muted' | 'simple' | undefined
|
package/types/category-card.d.ts
CHANGED
package/types/playlist-card.d.ts
CHANGED