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