itube-specs 0.0.893 → 0.0.896
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/scss/themes/dark/_theme.scss +16 -12
- package/assets/scss/themes/light/_theme.scss +16 -12
- package/components/card/video/index.vue +17 -2
- package/components/library/feed.vue +340 -0
- package/components/library/follows.vue +147 -0
- package/components/ui/expand-row.vue +21 -2
- package/package.json +1 -1
- package/pages/library/categories.vue +61 -0
- package/pages/library/channels.vue +62 -0
- package/pages/library/index.vue +2 -2
- package/pages/library/models.vue +25 -130
- package/stubs/library-follows.ts +135 -0
- package/types/index.d.ts +1 -0
- package/types/library-feed.d.ts +34 -0
- package/components/library/model.vue +0 -307
- package/stubs/library-models.ts +0 -75
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
<!-- Блок подписки на модель: шапка с аватаром, тегами и счётчиками + лента последних роликов -->
|
|
2
|
-
<template>
|
|
3
|
-
<article class="library-model">
|
|
4
|
-
<header class="library-model__header">
|
|
5
|
-
<NuxtLink
|
|
6
|
-
:to="link"
|
|
7
|
-
:title="model.title"
|
|
8
|
-
class="library-model__avatar-link"
|
|
9
|
-
>
|
|
10
|
-
<UiImg
|
|
11
|
-
:src="currentImage"
|
|
12
|
-
:alt="model.title"
|
|
13
|
-
:loading="priority ? 'eager' : 'lazy'"
|
|
14
|
-
:priority="priority"
|
|
15
|
-
class="library-model__avatar"
|
|
16
|
-
width="96"
|
|
17
|
-
height="96"
|
|
18
|
-
sizes="96px"
|
|
19
|
-
@error="imageError = true"
|
|
20
|
-
/>
|
|
21
|
-
</NuxtLink>
|
|
22
|
-
|
|
23
|
-
<div class="library-model__info">
|
|
24
|
-
<div class="library-model__heading">
|
|
25
|
-
<NuxtLink
|
|
26
|
-
:to="link"
|
|
27
|
-
class="library-model__name"
|
|
28
|
-
>
|
|
29
|
-
{{ model.title }}
|
|
30
|
-
<img
|
|
31
|
-
v-if="model.country"
|
|
32
|
-
:alt="model.country.title"
|
|
33
|
-
:title="`${t('birthplace_country')}: ${model.country.title}`"
|
|
34
|
-
:src="`/icons/flags/${model.country.name}.svg`"
|
|
35
|
-
class="library-model__flag"
|
|
36
|
-
width="20"
|
|
37
|
-
height="14"
|
|
38
|
-
>
|
|
39
|
-
</NuxtLink>
|
|
40
|
-
|
|
41
|
-
<span
|
|
42
|
-
v-if="model.newCount"
|
|
43
|
-
class="library-model__badge"
|
|
44
|
-
>{{ t('library.new_count', { count: model.newCount }) }}</span>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<p
|
|
48
|
-
v-if="model.tags.length"
|
|
49
|
-
class="library-model__tags"
|
|
50
|
-
>{{ model.tags.join(' · ') }}</p>
|
|
51
|
-
|
|
52
|
-
<p class="library-model__meta">
|
|
53
|
-
<span class="library-model__meta-item">
|
|
54
|
-
<UiIcon
|
|
55
|
-
name="video-camera"
|
|
56
|
-
size="14"
|
|
57
|
-
/>
|
|
58
|
-
<span class="library-model__meta-videos">{{ $n(model.videosCount, 'compact') }}</span>
|
|
59
|
-
{{ model.age }} {{ t('yo') }}
|
|
60
|
-
</span>
|
|
61
|
-
|
|
62
|
-
<span class="library-model__meta-item">
|
|
63
|
-
<UiIcon
|
|
64
|
-
name="date"
|
|
65
|
-
size="14"
|
|
66
|
-
/>
|
|
67
|
-
{{ t('library.updated_at', { date: formatDate(model.lastVideoAddedAt) }) }}
|
|
68
|
-
</span>
|
|
69
|
-
</p>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<div class="library-model__actions _from-sm">
|
|
73
|
-
<UiBtn
|
|
74
|
-
:theme="following ? 'secondary' : 'primary'"
|
|
75
|
-
class="library-model__follow"
|
|
76
|
-
@click="emit('toggle-follow', model)"
|
|
77
|
-
>
|
|
78
|
-
<UiIcon
|
|
79
|
-
:name="following ? 'check' : 'plus'"
|
|
80
|
-
size="16"
|
|
81
|
-
/>
|
|
82
|
-
{{ following ? t('library.following_state') : t('follow') }}
|
|
83
|
-
</UiBtn>
|
|
84
|
-
|
|
85
|
-
<UiLink
|
|
86
|
-
:to="link"
|
|
87
|
-
theme="secondary"
|
|
88
|
-
>
|
|
89
|
-
{{ t('plural.videos', model.videosCount) }}
|
|
90
|
-
<UiIcon
|
|
91
|
-
name="arrow-right"
|
|
92
|
-
size="16"
|
|
93
|
-
/>
|
|
94
|
-
</UiLink>
|
|
95
|
-
</div>
|
|
96
|
-
</header>
|
|
97
|
-
|
|
98
|
-
<div class="library-model__grid">
|
|
99
|
-
<CardVideo
|
|
100
|
-
v-for="(video, index) in model.videos"
|
|
101
|
-
:key="video.guid"
|
|
102
|
-
:card="video"
|
|
103
|
-
:loading="priority && index < 3 ? 'eager' : 'lazy'"
|
|
104
|
-
:priority="priority && index < 3"
|
|
105
|
-
compact
|
|
106
|
-
/>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<UiLink
|
|
110
|
-
:to="link"
|
|
111
|
-
theme="secondary"
|
|
112
|
-
class="library-model__all _to-sm"
|
|
113
|
-
wide
|
|
114
|
-
>
|
|
115
|
-
{{ t('plural.videos', model.videosCount) }}
|
|
116
|
-
<UiIcon
|
|
117
|
-
name="arrow-right"
|
|
118
|
-
size="16"
|
|
119
|
-
/>
|
|
120
|
-
</UiLink>
|
|
121
|
-
</article>
|
|
122
|
-
</template>
|
|
123
|
-
|
|
124
|
-
<script setup lang="ts">
|
|
125
|
-
import { convertString, formatDate } from '../../runtime';
|
|
126
|
-
import type { LibraryModelStub } from '../../stubs/library-models';
|
|
127
|
-
|
|
128
|
-
const props = defineProps<{
|
|
129
|
-
model: LibraryModelStub
|
|
130
|
-
following?: boolean
|
|
131
|
-
priority?: boolean
|
|
132
|
-
}>();
|
|
133
|
-
|
|
134
|
-
const emit = defineEmits<{
|
|
135
|
-
(eventName: 'toggle-follow', model: LibraryModelStub): void,
|
|
136
|
-
}>();
|
|
137
|
-
|
|
138
|
-
const { t } = useI18n();
|
|
139
|
-
const { generateLink } = useGenerateLink();
|
|
140
|
-
|
|
141
|
-
const link = computed(() => generateLink(`/models/${convertString().toSlug(props.model.title)}`));
|
|
142
|
-
|
|
143
|
-
const fallbackImage = '/img/model-placeholder.webp';
|
|
144
|
-
const imageError = ref(false);
|
|
145
|
-
|
|
146
|
-
const currentImage = computed(() => imageError.value ? fallbackImage : props.model.primaryImageUrl);
|
|
147
|
-
</script>
|
|
148
|
-
|
|
149
|
-
<style lang="scss">
|
|
150
|
-
.library-model {
|
|
151
|
-
padding: 16px;
|
|
152
|
-
background-color: $library-model-bg;
|
|
153
|
-
border: 1px solid $library-model-border;
|
|
154
|
-
border-radius: $radius-xl;
|
|
155
|
-
|
|
156
|
-
@include from-br(sm) {
|
|
157
|
-
padding: 20px;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.library-model__header {
|
|
162
|
-
display: grid;
|
|
163
|
-
grid-template-columns: auto 1fr;
|
|
164
|
-
column-gap: 16px;
|
|
165
|
-
row-gap: 12px;
|
|
166
|
-
align-items: start;
|
|
167
|
-
margin-bottom: 20px;
|
|
168
|
-
|
|
169
|
-
@include from-br(sm) {
|
|
170
|
-
grid-template-columns: auto 1fr auto;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.library-model__avatar {
|
|
175
|
-
width: 80px;
|
|
176
|
-
height: 80px;
|
|
177
|
-
object-fit: cover;
|
|
178
|
-
object-position: center 30%;
|
|
179
|
-
border-radius: $radius-full;
|
|
180
|
-
border: 2px solid $library-model-avatar-border;
|
|
181
|
-
|
|
182
|
-
@include from-br(sm) {
|
|
183
|
-
width: 96px;
|
|
184
|
-
height: 96px;
|
|
185
|
-
transition: var(--transition) border-color;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
@include from-br(sm) {
|
|
190
|
-
.library-model__avatar-link:is(:hover, :focus-visible) .library-model__avatar {
|
|
191
|
-
border-color: $library-model-avatar-border--hover;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.library-model__info {
|
|
196
|
-
min-width: 0;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.library-model__heading {
|
|
200
|
-
display: flex;
|
|
201
|
-
flex-wrap: wrap;
|
|
202
|
-
align-items: center;
|
|
203
|
-
column-gap: 10px;
|
|
204
|
-
row-gap: 6px;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.library-model__name {
|
|
208
|
-
@include font-lg;
|
|
209
|
-
|
|
210
|
-
display: flex;
|
|
211
|
-
align-items: center;
|
|
212
|
-
column-gap: 8px;
|
|
213
|
-
font-weight: 700;
|
|
214
|
-
text-transform: capitalize;
|
|
215
|
-
color: $library-model-name;
|
|
216
|
-
|
|
217
|
-
@include from-br(sm) {
|
|
218
|
-
transition: var(--transition) color;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
@include hover(color) {
|
|
222
|
-
color: $library-model-name--hover;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.library-model__flag {
|
|
227
|
-
flex-shrink: 0;
|
|
228
|
-
border-radius: 2px;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.library-model__badge {
|
|
232
|
-
@include font-3xs;
|
|
233
|
-
|
|
234
|
-
flex-shrink: 0;
|
|
235
|
-
font-weight: 600;
|
|
236
|
-
white-space: nowrap;
|
|
237
|
-
padding-block: 4px;
|
|
238
|
-
padding-inline: 10px;
|
|
239
|
-
color: $library-model-badge-text;
|
|
240
|
-
background-color: $library-model-badge-bg;
|
|
241
|
-
border-radius: $radius-full;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.library-model__tags {
|
|
245
|
-
@include font-sm;
|
|
246
|
-
|
|
247
|
-
margin-top: 4px;
|
|
248
|
-
color: $library-model-tags;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.library-model__meta {
|
|
252
|
-
@include font-sm;
|
|
253
|
-
|
|
254
|
-
display: flex;
|
|
255
|
-
flex-wrap: wrap;
|
|
256
|
-
align-items: center;
|
|
257
|
-
column-gap: 16px;
|
|
258
|
-
row-gap: 4px;
|
|
259
|
-
margin-top: 2px;
|
|
260
|
-
color: $library-model-meta;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.library-model__meta-item {
|
|
264
|
-
display: flex;
|
|
265
|
-
align-items: center;
|
|
266
|
-
column-gap: 6px;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.library-model__meta-videos::after {
|
|
270
|
-
content: '·';
|
|
271
|
-
margin-inline-start: 6px;
|
|
272
|
-
color: $library-model-meta-dot;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.library-model__actions {
|
|
276
|
-
display: flex;
|
|
277
|
-
align-items: center;
|
|
278
|
-
column-gap: 8px;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.library-model__follow {
|
|
282
|
-
min-width: 124px;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.library-model__grid {
|
|
286
|
-
display: grid;
|
|
287
|
-
gap: 16px;
|
|
288
|
-
grid-template-columns: repeat(2, 1fr);
|
|
289
|
-
|
|
290
|
-
@include from-br(xm) {
|
|
291
|
-
grid-template-columns: repeat(3, 1fr);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
@include from-br(sm) {
|
|
295
|
-
gap: 24px 16px;
|
|
296
|
-
grid-template-columns: repeat(4, 1fr);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
@include from-br(md) {
|
|
300
|
-
grid-template-columns: repeat(5, 1fr);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
.library-model__all {
|
|
305
|
-
margin-top: 16px;
|
|
306
|
-
}
|
|
307
|
-
</style>
|
package/stubs/library-models.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { VIDEO_POOL } from './videos';
|
|
2
|
-
import type { StubVideo } from './videos';
|
|
3
|
-
|
|
4
|
-
const HOUR = 3600;
|
|
5
|
-
const DAY = 24 * HOUR;
|
|
6
|
-
|
|
7
|
-
/** Ролик в ленте подписки: пул + флаг «новинки» и уникальный guid внутри модели. */
|
|
8
|
-
export type LibraryModelVideo = StubVideo & {
|
|
9
|
-
guid: string;
|
|
10
|
-
isNew: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type LibraryModelStub = {
|
|
14
|
-
guid: string;
|
|
15
|
-
title: string;
|
|
16
|
-
primaryImageUrl: string;
|
|
17
|
-
country: {
|
|
18
|
-
name: string
|
|
19
|
-
title: string
|
|
20
|
-
};
|
|
21
|
-
age: number;
|
|
22
|
-
videosCount: number;
|
|
23
|
-
/** Сколько роликов вышло с прошлого захода */
|
|
24
|
-
newCount: number;
|
|
25
|
-
/** Unix timestamp (сек.) последнего добавленного ролика */
|
|
26
|
-
lastVideoAddedAt: number;
|
|
27
|
-
tags: string[];
|
|
28
|
-
videos: LibraryModelVideo[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Сколько последних роликов модели показываем в ленте. */
|
|
32
|
-
const PREVIEW_LIMIT = 5;
|
|
33
|
-
|
|
34
|
-
/** Порядок обновлений намеренно не совпадает с алфавитным — иначе обе сортировки дают одно и то же. */
|
|
35
|
-
const MODELS = [
|
|
36
|
-
{ title: 'Dakota Lovell', image: 'Dakota_Lovell.png', country: { name: 'us', title: 'USA' }, age: 22, videosCount: 41, newCount: 3, tags: ['Twink', 'Jock', 'Versatile'], updatedAgo: 6 * HOUR, videoOffset: 3 },
|
|
37
|
-
{ title: 'Jax Thirio', image: 'Jax_Thirio.png', country: { name: 'ca', title: 'Canada' }, age: 38, videosCount: 17, newCount: 2, tags: ['Muscle', 'Daddy', 'Top'], updatedAgo: 2 * DAY, videoOffset: 9 },
|
|
38
|
-
{ title: 'Austin Young', image: 'Austin_Young.png', country: { name: 'us', title: 'USA' }, age: 24, videosCount: 75, newCount: 1, tags: ['Twink', 'Smooth', 'Bottom'], updatedAgo: 9 * DAY, videoOffset: 0 },
|
|
39
|
-
{ title: 'Devin Franco', image: 'Devin_Franco.png', country: { name: 'us', title: 'USA' }, age: 33, videosCount: 24, newCount: 0, tags: ['Muscle', 'Tattooed', 'Bottom'], updatedAgo: 21 * DAY, videoOffset: 6 },
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Подписки пользователя на моделей. Ручки под них пока нет — вёрстка живёт на заглушке.
|
|
44
|
-
* Возраст ролика считаем от «сейчас», чтобы даты в разметке не устаревали.
|
|
45
|
-
*/
|
|
46
|
-
export function getLibraryModelsStub(): LibraryModelStub[] {
|
|
47
|
-
const now = Math.floor(Date.now() / 1000);
|
|
48
|
-
|
|
49
|
-
return MODELS.map((model) => {
|
|
50
|
-
const slug = model.title.toLowerCase().replace(/\s+/g, '-');
|
|
51
|
-
|
|
52
|
-
const videos = Array.from({ length: PREVIEW_LIMIT }, (_, index) => {
|
|
53
|
-
const video = VIDEO_POOL[(model.videoOffset + index) % VIDEO_POOL.length];
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
...video,
|
|
57
|
-
guid: `${slug}-${video.id}`,
|
|
58
|
-
isNew: index < model.newCount,
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
guid: slug,
|
|
64
|
-
title: model.title,
|
|
65
|
-
primaryImageUrl: `/img/models/${model.image}`,
|
|
66
|
-
country: model.country,
|
|
67
|
-
age: model.age,
|
|
68
|
-
videosCount: model.videosCount,
|
|
69
|
-
newCount: model.newCount,
|
|
70
|
-
lastVideoAddedAt: now - model.updatedAgo,
|
|
71
|
-
tags: model.tags,
|
|
72
|
-
videos,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
}
|