itube-specs 0.0.607 → 0.0.608
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/components/page-components/s-expand-row.vue +4 -1
- package/components/page-components/s-filter-button.vue +1 -1
- package/components/page-components/s-filter-chips.vue +0 -2
- package/components/page-components/s-filter-page.vue +2 -2
- package/components/page-components/s-info-grid.vue +40 -21
- package/components/page-components/s-like.vue +117 -0
- package/components/page-components/s-model-filters.vue +41 -40
- package/components/page-components/s-report.vue +133 -102
- package/components/page-components/s-share.vue +36 -22
- package/components/playlist/s-playlist-add.vue +103 -63
- package/components/ui/s-checkbox.vue +1 -1
- package/components/ui/s-slider.vue +6 -3
- package/components/ui/s-snackbar.vue +10 -7
- package/components/ui/s-timestamp.vue +10 -2
- package/composables/use-api-fetcher.ts +1 -12
- package/composables/use-favorites.ts +19 -0
- package/composables/use-navigation-items.ts +84 -0
- package/package.json +1 -1
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
class="s-expand-row__wrapper"
|
|
13
13
|
:class="[{ '--more-show': isElementsOverflow }]"
|
|
14
14
|
>
|
|
15
|
-
<slot name="prepend"
|
|
15
|
+
<slot name="prepend">
|
|
16
|
+
<span v-if="preText" class="s-expand-row__pre-text">{{ preText }}</span>
|
|
17
|
+
</slot>
|
|
16
18
|
<slot>
|
|
17
19
|
<SChips
|
|
18
20
|
v-for="item in itemsResult"
|
|
@@ -54,6 +56,7 @@ const props = withDefaults(
|
|
|
54
56
|
mini?: boolean;
|
|
55
57
|
mobileExpand?: boolean;
|
|
56
58
|
maximumButtons?: number;
|
|
59
|
+
preText?: string;
|
|
57
60
|
}>(), {
|
|
58
61
|
maximumButtons: 20, // вычислено на глаз, приблизительно
|
|
59
62
|
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
:model-value="getSelectValue(item.name)"
|
|
33
33
|
:items="selectItems(item)"
|
|
34
34
|
:count="getCount(item)"
|
|
35
|
+
size="s"
|
|
35
36
|
:active="isActiveSelect(item.name)"
|
|
36
37
|
:label="item.title"
|
|
37
38
|
@update:model-value="val => updateSelectFilter(item.name, val)"
|
|
@@ -44,10 +45,9 @@
|
|
|
44
45
|
</template>
|
|
45
46
|
|
|
46
47
|
<script setup lang="ts">
|
|
47
|
-
import type { IModelFilter
|
|
48
|
+
import type { IModelFilter } from '../../types';
|
|
48
49
|
|
|
49
50
|
import { useRoute, useRouter } from 'vue-router';
|
|
50
|
-
import { getMonth, isMobileWidth } from '../../runtime';
|
|
51
51
|
|
|
52
52
|
const props = defineProps<{
|
|
53
53
|
filters: IModelFilter[]
|
|
@@ -1,44 +1,63 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="s-info-grid">
|
|
3
3
|
<div
|
|
4
|
-
v-for="(
|
|
5
|
-
:key="`s-info-grid-
|
|
6
|
-
class="s-info-
|
|
4
|
+
v-for="(group, gIndex) in groups"
|
|
5
|
+
:key="`s-info-grid-group-${gIndex}`"
|
|
6
|
+
class="s-info-grid__group"
|
|
7
7
|
>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
8
|
+
<h3 class="s-info-grid__group-title">{{ group.title }}</h3>
|
|
9
|
+
<div class="s-info-grid__group-items">
|
|
10
10
|
<NuxtLink
|
|
11
|
-
v-for="(
|
|
12
|
-
:key="`
|
|
13
|
-
class="s-info-grid__item
|
|
14
|
-
:to="generateLink(link(item,
|
|
15
|
-
>
|
|
16
|
-
|
|
11
|
+
v-for="(item, index) in group.items"
|
|
12
|
+
:key="`s-info-grid-item-${gIndex}-${index}`"
|
|
13
|
+
class="s-info-grid__item"
|
|
14
|
+
:to="generateLink(link(item, item.values[0]))"
|
|
15
|
+
>
|
|
16
|
+
<SIcon class="s-info-grid__item-icon" name="tattoos" size="12" />
|
|
17
|
+
<span class="s-info-grid__item-title">{{ item.title }}</span>
|
|
18
|
+
<p class="s-info-grid__item-values">
|
|
19
|
+
<span
|
|
20
|
+
v-for="(value, vIndex) in item.values"
|
|
21
|
+
:key="`model-value-${gIndex}-${index}-${vIndex}`"
|
|
22
|
+
class="s-info-grid__item-value"
|
|
23
|
+
:class="[
|
|
24
|
+
{'--success': isSuccess(value)},
|
|
25
|
+
{'--warning': isWarning(value)}
|
|
26
|
+
]"
|
|
27
|
+
>{{ value }}{{ vIndex < item.values.length - 1 ? ', ' : '' }}</span>
|
|
28
|
+
</p>
|
|
29
|
+
</NuxtLink>
|
|
30
|
+
</div>
|
|
17
31
|
</div>
|
|
18
32
|
</div>
|
|
19
33
|
</template>
|
|
20
34
|
|
|
21
35
|
<script setup lang="ts">
|
|
22
|
-
import type {
|
|
36
|
+
import type { IGroupedParameter, IGroupedParameterItem } from 'itube-specs/types';
|
|
23
37
|
|
|
24
38
|
defineProps<{
|
|
25
|
-
|
|
39
|
+
groups: IGroupedParameter[]
|
|
26
40
|
}>();
|
|
27
41
|
|
|
28
|
-
function getValue(value: IParameterModelValue[]) {
|
|
29
|
-
return value.map(item => item);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
42
|
const {generateLink} = useGenerateLink();
|
|
33
43
|
|
|
34
|
-
function link(item:
|
|
44
|
+
function link(item: IGroupedParameterItem, value: string) {
|
|
35
45
|
const formattedValue = value.toLowerCase().replace(/\s+/g, '+');
|
|
36
|
-
const groupNumber = `&group=${item.group.order}`;
|
|
37
46
|
|
|
38
47
|
if (item.kind === 'range') {
|
|
39
|
-
return `/models?filter_${item.name}_from=${formattedValue}&filter_${item.name}_to=${formattedValue}
|
|
48
|
+
return `/models?filter_${item.name}_from=${formattedValue}&filter_${item.name}_to=${formattedValue}`;
|
|
40
49
|
} else {
|
|
41
|
-
return `/models?filter_${item.name}=${formattedValue}
|
|
50
|
+
return `/models?filter_${item.name}=${formattedValue}`;
|
|
42
51
|
}
|
|
43
52
|
}
|
|
53
|
+
|
|
54
|
+
function isSuccess(value: string) {
|
|
55
|
+
const values = ['active'];
|
|
56
|
+
return values.includes(value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isWarning(value: string) {
|
|
60
|
+
const values = ['inactive'];
|
|
61
|
+
return values.includes(value)
|
|
62
|
+
}
|
|
44
63
|
</script>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="s-like">
|
|
3
|
+
<button
|
|
4
|
+
class="s-like__control"
|
|
5
|
+
:class="{'--active': isLiked}"
|
|
6
|
+
type="button"
|
|
7
|
+
:title="t('like')"
|
|
8
|
+
@click="onReactionClick('like')"
|
|
9
|
+
>
|
|
10
|
+
<span class="s-like__control-icon-wrapper">
|
|
11
|
+
<SIcon class="s-like__control-icon --like" name="thumbs-up" size="16"/>
|
|
12
|
+
</span>
|
|
13
|
+
<span class="s-like__control-text">{{ formatNumber(Number(resultLikes)) }}</span>
|
|
14
|
+
</button>
|
|
15
|
+
<div
|
|
16
|
+
v-if="bar"
|
|
17
|
+
class="s-like__bar"
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
class="s-like__bar-likes"
|
|
21
|
+
:style="{ width: likePercent + '%' }"
|
|
22
|
+
/>
|
|
23
|
+
<div
|
|
24
|
+
class="s-like__bar-dislikes"
|
|
25
|
+
:style="{ width: dislikePercent + '%' }"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
<button
|
|
29
|
+
class="s-like__control"
|
|
30
|
+
:class="{'--active': isDisliked}"
|
|
31
|
+
type="button"
|
|
32
|
+
:title="t('dislike')"
|
|
33
|
+
@click="onReactionClick('dislike')"
|
|
34
|
+
>
|
|
35
|
+
<span class="s-like__control-icon-wrapper">
|
|
36
|
+
<SIcon class="s-like__control-icon --dislike" name="thumbs-down" size="16"/>
|
|
37
|
+
</span>
|
|
38
|
+
<span class="s-like__control-text">{{ formatNumber(Number(resultDislikes)) }}</span>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { formatNumber } from 'itube-specs/runtime';
|
|
45
|
+
import { VideosApiService } from '~/services/api/videos.service';
|
|
46
|
+
import { PlaylistsApiService } from '~/services/api/playlists.service';
|
|
47
|
+
|
|
48
|
+
const { t } = useI18n();
|
|
49
|
+
|
|
50
|
+
const props = defineProps<{
|
|
51
|
+
id?: string
|
|
52
|
+
likes: number
|
|
53
|
+
dislikes: number
|
|
54
|
+
bar?: boolean
|
|
55
|
+
videoMd5?: string
|
|
56
|
+
}>()
|
|
57
|
+
|
|
58
|
+
const { execute: executeVideoLike } = useApiAction(VideosApiService.videoLike);
|
|
59
|
+
const { execute: executeVideoDislike } = useApiAction(VideosApiService.videoDislike);
|
|
60
|
+
const { execute: executePlaylistLike } = useApiAction(PlaylistsApiService.playlistLike);
|
|
61
|
+
const { execute: executePlaylistDislike } = useApiAction(PlaylistsApiService.playlistDislike);
|
|
62
|
+
|
|
63
|
+
const storageKey = computed(() => props.videoMd5 || props.id || '');
|
|
64
|
+
|
|
65
|
+
const likeStatus = ref<'like' | 'dislike' | null>(null);
|
|
66
|
+
const isLiked = computed(() => likeStatus.value === 'like');
|
|
67
|
+
const isDisliked = computed(() => likeStatus.value === 'dislike');
|
|
68
|
+
const resultLikes = ref(props.likes);
|
|
69
|
+
const resultDislikes = ref(props.dislikes);
|
|
70
|
+
|
|
71
|
+
onMounted(() => {
|
|
72
|
+
const stored = localStorage.getItem(storageKey.value);
|
|
73
|
+
if (stored === 'like' || stored === 'dislike') {
|
|
74
|
+
likeStatus.value = stored;
|
|
75
|
+
if (stored === 'like') resultLikes.value += 1;
|
|
76
|
+
if (stored === 'dislike') resultDislikes.value += 1;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function onReactionClick(type: 'like' | 'dislike') {
|
|
81
|
+
if (likeStatus.value !== type) {
|
|
82
|
+
likeStatus.value = type;
|
|
83
|
+
localStorage.setItem(storageKey.value, type);
|
|
84
|
+
|
|
85
|
+
const isLike = type === 'like';
|
|
86
|
+
|
|
87
|
+
if (isLike) {
|
|
88
|
+
if (resultLikes.value < 1000) resultLikes.value += 1;
|
|
89
|
+
if (resultDislikes.value > 0) resultDislikes.value -= 1;
|
|
90
|
+
} else {
|
|
91
|
+
if (resultDislikes.value < 1000) resultDislikes.value += 1;
|
|
92
|
+
if (resultLikes.value > 0) resultLikes.value -= 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (props.videoMd5) {
|
|
96
|
+
if (isLike) {
|
|
97
|
+
await executeVideoLike([props.videoMd5]);
|
|
98
|
+
} else {
|
|
99
|
+
await executeVideoDislike([props.videoMd5]);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
if (isLike) {
|
|
103
|
+
await executePlaylistLike([props.id!]);
|
|
104
|
+
} else {
|
|
105
|
+
await executePlaylistDislike([props.id!]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const likes = computed(() => resultLikes.value || 0);
|
|
112
|
+
const dislikes = computed(() => resultDislikes.value || 0);
|
|
113
|
+
const total = computed(() => likes.value + dislikes.value || 1);
|
|
114
|
+
|
|
115
|
+
const likePercent = computed(() => (likes.value / total.value) * 100);
|
|
116
|
+
const dislikePercent = computed(() => (dislikes.value / total.value) * 100);
|
|
117
|
+
</script>
|
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
2
|
+
<div class="s-model-filters">
|
|
3
|
+
<SFilterChips
|
|
4
|
+
:items="chipsItems"
|
|
5
|
+
@click="(e: IChipsItem)=> onChipsClick(e)"
|
|
6
|
+
/>
|
|
7
|
+
<transition>
|
|
8
|
+
<SPopup
|
|
9
|
+
v-if="model"
|
|
10
|
+
v-model="model"
|
|
11
|
+
class="s-model-filters__popup"
|
|
12
|
+
>
|
|
13
|
+
<template #title>{{ popupTitle }}</template>
|
|
14
|
+
<SFilterPage
|
|
15
|
+
class="s-model-filters__filters"
|
|
16
|
+
:filters="data.filters"
|
|
17
|
+
:count="data.total"
|
|
18
|
+
:class="{'_loading': status === 'pending'}"
|
|
19
|
+
/>
|
|
20
|
+
<template #footer>
|
|
21
|
+
<div class="s-model-filters__footer-buttons">
|
|
22
|
+
<SButton
|
|
23
|
+
wide
|
|
24
|
+
class="s-model-filters__reset"
|
|
25
|
+
:disabled="!isFiltered"
|
|
26
|
+
size="l"
|
|
27
|
+
theme="secondary"
|
|
28
|
+
@click="onResetClick"
|
|
29
|
+
>{{ t('reset_all') }}
|
|
30
|
+
</SButton>
|
|
31
|
+
<SButton
|
|
32
|
+
wide
|
|
33
|
+
size="l"
|
|
34
|
+
theme="primary"
|
|
35
|
+
@click="onShowClick"
|
|
36
|
+
>{{ t('show_results') }}
|
|
37
|
+
</SButton>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
</SPopup>
|
|
41
|
+
</transition>
|
|
42
|
+
</div>
|
|
42
43
|
</template>
|
|
43
44
|
|
|
44
45
|
<script setup lang="ts">
|
|
@@ -3,9 +3,40 @@
|
|
|
3
3
|
<SPopup
|
|
4
4
|
v-if="isReportPopupOpen"
|
|
5
5
|
v-model="isReportPopupOpen"
|
|
6
|
+
sheet
|
|
7
|
+
back
|
|
8
|
+
class="s-report"
|
|
9
|
+
:class="{'--back-show': activeStep !== 'list'}"
|
|
10
|
+
@back="activeStep === 'list' ? handleBack() : goToList()"
|
|
6
11
|
>
|
|
7
12
|
<template #title>{{ t('report') }}</template>
|
|
13
|
+
|
|
14
|
+
<!-- Step 1: Category list -->
|
|
15
|
+
<div v-if="activeStep === 'list'" class="s-report__popup-wrapper">
|
|
16
|
+
<SVideoMiniCard
|
|
17
|
+
class="s-report__video-card"
|
|
18
|
+
:card="reportedVideoCard"
|
|
19
|
+
/>
|
|
20
|
+
<div class="s-report__categories">
|
|
21
|
+
<button
|
|
22
|
+
v-for="(item, index) in reportFormsScheme"
|
|
23
|
+
:key="`report-category-${index}`"
|
|
24
|
+
class="s-report__category-button"
|
|
25
|
+
@click="selectCategory(item.subject)"
|
|
26
|
+
>
|
|
27
|
+
{{ t(`report_form.${item.title}`) }}
|
|
28
|
+
<SIcon
|
|
29
|
+
class="s-report__category-icon"
|
|
30
|
+
name="chevron-right"
|
|
31
|
+
size="20"
|
|
32
|
+
/>
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Step 2: Report form -->
|
|
8
38
|
<div
|
|
39
|
+
v-else
|
|
9
40
|
class="s-report__popup-wrapper"
|
|
10
41
|
:class="{'_loading': loading}"
|
|
11
42
|
>
|
|
@@ -13,107 +44,88 @@
|
|
|
13
44
|
class="s-report__video-card"
|
|
14
45
|
:card="reportedVideoCard"
|
|
15
46
|
/>
|
|
16
|
-
<div class="s-
|
|
17
|
-
<
|
|
18
|
-
v-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
<div class="s-report__wrapper" v-if="activeCategory">
|
|
48
|
+
<p
|
|
49
|
+
v-if="activeCategory.text"
|
|
50
|
+
class="s-report__form-text"
|
|
51
|
+
>{{ t(`report_form.${activeCategory.text}`) }}
|
|
52
|
+
</p>
|
|
53
|
+
<p
|
|
54
|
+
v-if="activeCategory.subtext"
|
|
55
|
+
class="s-report__form-text"
|
|
56
|
+
>{{ t(`report_form.${activeCategory.subtext}`) }}
|
|
57
|
+
</p>
|
|
58
|
+
<ul
|
|
59
|
+
v-if="activeCategory.list && activeCategory.list.length > 0"
|
|
60
|
+
class="s-report__form-menu"
|
|
23
61
|
>
|
|
24
|
-
<
|
|
25
|
-
|
|
62
|
+
<li
|
|
63
|
+
v-for="(subItem, subIndex) in activeCategory.list"
|
|
64
|
+
:key="`report-list-${subIndex}`"
|
|
65
|
+
class="s-report__form-list"
|
|
66
|
+
>{{ t(`report_form.${subItem.text}`) }}
|
|
67
|
+
</li>
|
|
68
|
+
</ul>
|
|
69
|
+
<form class="s-report__form-wrapper">
|
|
70
|
+
<div
|
|
71
|
+
v-for="(subItem, subIndex) in activeCategory.items"
|
|
72
|
+
:key="`${subItem.label}-${subIndex}`"
|
|
73
|
+
:class="[{'--wide': subItem.wide},subItem.marginClass]"
|
|
26
74
|
>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
75
|
+
<SInput
|
|
76
|
+
v-if="['text', 'tel', 'textarea'].includes(subItem.type)"
|
|
77
|
+
v-model="form.data[subItem.value] as string"
|
|
78
|
+
:label="subItem.label ? t(`report_form.${subItem.label}`) : undefined"
|
|
79
|
+
:hide-label="subItem.hideLabel"
|
|
80
|
+
:type="subItem.type as InputTypes"
|
|
81
|
+
:placeholder="subItem.placeholder ? t(`report_form.${subItem.placeholder}`) : undefined"
|
|
82
|
+
:required="subItem.required"
|
|
83
|
+
:error="error[subItem.value]"
|
|
84
|
+
@update:error="(val: boolean) => error[subItem.value] = val"
|
|
32
85
|
/>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</ul>
|
|
54
|
-
<form class="s-report__form-wrapper">
|
|
55
|
-
<template
|
|
56
|
-
v-for="(subItem, subIndex) in item.items"
|
|
57
|
-
:key="`${subItem.label}-${subIndex}`"
|
|
58
|
-
>
|
|
59
|
-
<SInput
|
|
60
|
-
v-if="['text', 'tel', 'textarea'].includes(subItem.type)"
|
|
61
|
-
:key="`report-form-${index}`"
|
|
62
|
-
v-model="form.data[subItem.value] as string"
|
|
63
|
-
:label=" t(`report_form.${subItem.label}`)"
|
|
64
|
-
:type="subItem.type as InputTypes"
|
|
65
|
-
:class="{'--wide': subItem.wide}"
|
|
66
|
-
:required="subItem.required"
|
|
67
|
-
:error="error[subItem.value]"
|
|
68
|
-
@update:error="(val: boolean) => error[subItem.value] = val"
|
|
69
|
-
/>
|
|
70
|
-
<SCheckbox
|
|
71
|
-
v-if="subItem.type === 'checkbox'"
|
|
72
|
-
v-model="form.data[subItem.value] as boolean"
|
|
73
|
-
class="--wide"
|
|
74
|
-
:required="subItem.required"
|
|
75
|
-
:error="error[subItem.value]"
|
|
76
|
-
@update:error="(val: boolean) => error[subItem.value] = val"
|
|
77
|
-
>{{ t(`report_form.${subItem.text}`) }}
|
|
78
|
-
</SCheckbox>
|
|
79
|
-
<SRadio
|
|
80
|
-
v-if="subItem.type === 'radio' "
|
|
81
|
-
v-model="reasonValue"
|
|
82
|
-
name="report-radio-value"
|
|
83
|
-
:value="subItem.value"
|
|
84
|
-
class="--wide"
|
|
85
|
-
:label=" t(`report_form.${subItem.text}`)"
|
|
86
|
-
:required="subItem.required"
|
|
87
|
-
:error="errorRadio"
|
|
88
|
-
@update:error="(val: boolean) => errorRadio = val"
|
|
89
|
-
/>
|
|
90
|
-
</template>
|
|
91
|
-
</form>
|
|
92
|
-
<SButton
|
|
93
|
-
:disabled="loading"
|
|
94
|
-
wide
|
|
95
|
-
size="l"
|
|
96
|
-
theme="primary"
|
|
97
|
-
@click="submit"
|
|
98
|
-
>{{ t('report_form.send_report') }}
|
|
99
|
-
</SButton>
|
|
100
|
-
</details>
|
|
86
|
+
<SCheckbox
|
|
87
|
+
v-if="subItem.type === 'checkbox'"
|
|
88
|
+
v-model="form.data[subItem.value] as boolean"
|
|
89
|
+
:required="subItem.required"
|
|
90
|
+
:error="error[subItem.value]"
|
|
91
|
+
@update:error="(val: boolean) => error[subItem.value] = val"
|
|
92
|
+
>{{ t(`report_form.${subItem.text}`) }}
|
|
93
|
+
</SCheckbox>
|
|
94
|
+
<SRadio
|
|
95
|
+
v-if="subItem.type === 'radio'"
|
|
96
|
+
v-model="reasonValue"
|
|
97
|
+
name="report-radio-value"
|
|
98
|
+
:value="subItem.value"
|
|
99
|
+
:label="t(`report_form.${subItem.text}`)"
|
|
100
|
+
:required="subItem.required"
|
|
101
|
+
:error="errorRadio"
|
|
102
|
+
@update:error="(val: boolean) => errorRadio = val"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</form>
|
|
101
106
|
</div>
|
|
102
|
-
<SButton
|
|
103
|
-
wide
|
|
104
|
-
size="l"
|
|
105
|
-
theme="secondary"
|
|
106
|
-
@click="closeReportPopup"
|
|
107
|
-
>{{ t('cancel') }}
|
|
108
|
-
</SButton>
|
|
109
107
|
</div>
|
|
108
|
+
|
|
109
|
+
<template v-if="activeStep !== 'list'" #footer>
|
|
110
|
+
<div class="s-report__send">
|
|
111
|
+
<SButton
|
|
112
|
+
class="s-report__send-button"
|
|
113
|
+
:disabled="loading"
|
|
114
|
+
wide
|
|
115
|
+
size="l"
|
|
116
|
+
theme="primary"
|
|
117
|
+
@click="submit"
|
|
118
|
+
>{{ t('report_form.send_report') }}
|
|
119
|
+
</SButton>
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
110
122
|
</SPopup>
|
|
111
123
|
</transition>
|
|
112
124
|
</template>
|
|
113
125
|
|
|
114
126
|
<script setup lang="ts">
|
|
115
|
-
import { reportFormsScheme } from '
|
|
116
|
-
import { EReportFormsSubjects, validateEmail, validatePhone } from '
|
|
127
|
+
import { reportFormsScheme } from 'itube-specs/lib/report-forms-scheme';
|
|
128
|
+
import { EReportFormsSubjects, validateEmail, validatePhone } from 'itube-specs/runtime';
|
|
117
129
|
import type { InputTypes, IReportForm, IReportRequest } from '../../types';
|
|
118
130
|
|
|
119
131
|
const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
|
|
@@ -124,6 +136,9 @@ const error = ref<Record<string, boolean>>({});
|
|
|
124
136
|
|
|
125
137
|
const { t } = useI18n();
|
|
126
138
|
|
|
139
|
+
type ReportStep = 'list' | EReportFormsSubjects;
|
|
140
|
+
const activeStep = ref<ReportStep>('list');
|
|
141
|
+
|
|
127
142
|
const form = ref<IReportForm>({
|
|
128
143
|
categoryName: EReportFormsSubjects.DMCA,
|
|
129
144
|
reason: '',
|
|
@@ -132,13 +147,38 @@ const form = ref<IReportForm>({
|
|
|
132
147
|
data: {},
|
|
133
148
|
})
|
|
134
149
|
|
|
135
|
-
const { isReportPopupOpen, closeReportPopup, reportedVideoCard } = useReportPopup();
|
|
150
|
+
const { isReportPopupOpen, closeReportPopup, reportedVideoCard, onBack } = useReportPopup();
|
|
151
|
+
|
|
152
|
+
function handleBack() {
|
|
153
|
+
const callback = onBack.value;
|
|
154
|
+
closeReportPopup();
|
|
155
|
+
callback?.();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function selectCategory(subject: EReportFormsSubjects) {
|
|
159
|
+
resetForm();
|
|
160
|
+
form.value.categoryName = subject;
|
|
161
|
+
activeStep.value = subject;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function goToList() {
|
|
165
|
+
resetForm();
|
|
166
|
+
activeStep.value = 'list';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
watch(isReportPopupOpen, (val) => {
|
|
170
|
+
if (val) {
|
|
171
|
+
resetForm();
|
|
172
|
+
} else {
|
|
173
|
+
activeStep.value = 'list';
|
|
174
|
+
}
|
|
175
|
+
});
|
|
136
176
|
|
|
137
177
|
const videoGuid = computed(() => reportedVideoCard.value.guid);
|
|
138
178
|
|
|
139
179
|
const loading = ref(false);
|
|
140
180
|
|
|
141
|
-
const activeCategory = computed(() => reportFormsScheme.find((subItem) => subItem.subject ===
|
|
181
|
+
const activeCategory = computed(() => reportFormsScheme.find((subItem) => subItem.subject === activeStep.value))
|
|
142
182
|
const requiredFields = computed(() => activeCategory.value?.items?.filter(item => item.required).map(item => item.value));
|
|
143
183
|
|
|
144
184
|
const { snackbarText, snackbarTheme, showErrorSnack, resetSnackbar } = useSnackbar();
|
|
@@ -228,13 +268,4 @@ async function submit() {
|
|
|
228
268
|
loading.value = false
|
|
229
269
|
}
|
|
230
270
|
}
|
|
231
|
-
|
|
232
|
-
function onToggle(event: Event, subject: EReportFormsSubjects) {
|
|
233
|
-
const el = event.target as HTMLDetailsElement;
|
|
234
|
-
if (el.open) {
|
|
235
|
-
form.value.categoryName = subject;
|
|
236
|
-
} else {
|
|
237
|
-
resetForm();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
271
|
</script>
|
|
@@ -21,35 +21,49 @@
|
|
|
21
21
|
:href="item.link"
|
|
22
22
|
>
|
|
23
23
|
<SImg
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
class="s-share__button-icon"
|
|
25
|
+
sizes="28px"
|
|
26
|
+
width="28"
|
|
27
|
+
height="28"
|
|
27
28
|
:src="`/img/socials/${item.img}`"
|
|
28
29
|
/>
|
|
29
30
|
</a>
|
|
30
31
|
</div>
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
<div class="s-share__wrapper">
|
|
33
|
+
<div class="s-share__copy">
|
|
34
|
+
<span class="s-share__copy-label _from-sm">{{ inputText }}</span>
|
|
35
|
+
<span class="s-share__copy-url">{{ fullUrl }}</span>
|
|
36
|
+
<button
|
|
37
|
+
class="s-share__copy-button"
|
|
38
|
+
:title="$t('copy')"
|
|
39
|
+
@click="copyUrl"
|
|
40
|
+
type="button"
|
|
41
|
+
>
|
|
42
|
+
<SIcon
|
|
43
|
+
class="s-share__copy-icon"
|
|
44
|
+
:class="{'--accent': isCopied}"
|
|
45
|
+
:name="copyIcon"
|
|
46
|
+
size="20"
|
|
47
|
+
/>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="s-share__cancel-wrapper">
|
|
51
|
+
<SButton
|
|
52
|
+
class="s-share__cancel"
|
|
53
|
+
wide
|
|
54
|
+
theme="secondary"
|
|
55
|
+
@click="closeSharePopup"
|
|
56
|
+
>{{ $t('cancel') }}
|
|
57
|
+
</SButton>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
46
60
|
</div>
|
|
47
61
|
</SPopup>
|
|
48
62
|
</transition>
|
|
49
63
|
</template>
|
|
50
64
|
|
|
51
65
|
<script setup lang="ts">
|
|
52
|
-
const {isSharePopupOpen, closeSharePopup, sharedVideoCard} = useSharePopup();
|
|
66
|
+
const { isSharePopupOpen, closeSharePopup, sharedVideoCard } = useSharePopup();
|
|
53
67
|
|
|
54
68
|
const route = useRoute();
|
|
55
69
|
|
|
@@ -95,9 +109,9 @@ const buttons = computed(() => {
|
|
|
95
109
|
text: 'Whatsapp',
|
|
96
110
|
},
|
|
97
111
|
{
|
|
98
|
-
img: '
|
|
99
|
-
link: `https://
|
|
100
|
-
text: '
|
|
112
|
+
img: 'x.svg',
|
|
113
|
+
link: `https://x.com/intent/tweet?url=${encodeURIComponent(fullUrl.value)}`,
|
|
114
|
+
text: 'X',
|
|
101
115
|
},
|
|
102
116
|
]
|
|
103
117
|
})
|
|
@@ -1,48 +1,91 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="isPlaylistAdd" class="s-playlist-add__decorate _to-sm"/>
|
|
3
2
|
<!-- Add-->
|
|
4
3
|
<transition mode="out-in">
|
|
5
4
|
<SPopup
|
|
6
|
-
|
|
5
|
+
class="s-playlist-add"
|
|
6
|
+
v-if="isPlaylistAdd"
|
|
7
7
|
v-model="isPlaylistAdd"
|
|
8
8
|
sheet
|
|
9
|
-
transparent-backdrop
|
|
10
9
|
@close="closePopup"
|
|
11
10
|
>
|
|
12
11
|
<template #title>{{ $t('playlist.add') }}</template>
|
|
13
12
|
|
|
14
|
-
<
|
|
15
|
-
class="s-playlist-add__wrapper"
|
|
16
|
-
:class="{'_loading': loadingUserPlaylists || loadingPostVideo}"
|
|
17
|
-
>
|
|
13
|
+
<template #fixedContent>
|
|
18
14
|
<SVideoMiniCard
|
|
19
15
|
v-if="videoCard"
|
|
20
16
|
class="s-playlist-add__mini-card"
|
|
21
17
|
:card="videoCard"
|
|
22
18
|
/>
|
|
19
|
+
</template>
|
|
20
|
+
<div
|
|
21
|
+
class="s-playlist-add__wrapper"
|
|
22
|
+
:class="{'_loading': loadingUserPlaylists || loadingPostVideo}"
|
|
23
|
+
>
|
|
23
24
|
<template
|
|
24
25
|
v-if="playlistsItems && playlistsItems.length"
|
|
25
26
|
>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
:
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
<SCheckbox
|
|
28
|
+
right
|
|
29
|
+
class="s-playlist-add__item"
|
|
30
|
+
:model-value="selectValue.includes('favorites')"
|
|
31
|
+
@update:model-value="togglePlaylist('favorites')"
|
|
32
|
+
>
|
|
33
|
+
<div class="s-playlist-add__item-wrapper --favorites">
|
|
34
|
+
<SIcon name="heart" size="20" />
|
|
35
|
+
{{ $t('favorites')}}
|
|
36
|
+
</div>
|
|
37
|
+
</SCheckbox>
|
|
38
|
+
<SCheckbox
|
|
39
|
+
v-for="(item, index) in playlistsItems"
|
|
40
|
+
class="s-playlist-add__item"
|
|
41
|
+
right
|
|
42
|
+
:key="`playlist-${index}`"
|
|
43
|
+
:model-value="selectValue.includes(item.value)"
|
|
44
|
+
@update:model-value="togglePlaylist(item.value)"
|
|
45
|
+
>
|
|
46
|
+
<div class="s-playlist-add__item-wrapper">
|
|
47
|
+
<SIcon
|
|
48
|
+
class="s-playlist-add__item-icon"
|
|
49
|
+
name="list"
|
|
50
|
+
size="20"
|
|
51
|
+
/>
|
|
52
|
+
<span class="s-playlist-add__item-text">{{ item.title }}</span>
|
|
53
|
+
<div class="s-playlist-add__item-icon">
|
|
54
|
+
<SIcon name="video-camera" size="16" />
|
|
55
|
+
{{ item.videosCount }}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</SCheckbox>
|
|
33
59
|
</template>
|
|
34
60
|
<button
|
|
61
|
+
v-if="step !== 'new'"
|
|
35
62
|
type="button"
|
|
36
|
-
class="s-playlist-
|
|
63
|
+
class="s-playlist-add__item --new"
|
|
37
64
|
@click="step = 'new'"
|
|
38
65
|
>
|
|
39
66
|
<SIcon
|
|
40
|
-
class="s-playlist-
|
|
41
|
-
name="
|
|
42
|
-
size="
|
|
67
|
+
class="s-playlist-add__item-icon --new"
|
|
68
|
+
name="plus"
|
|
69
|
+
size="20"
|
|
43
70
|
/>
|
|
44
71
|
{{ $t('playlist.new_playlist') }}
|
|
45
72
|
</button>
|
|
73
|
+
<div v-else class="s-playlist-add__new">
|
|
74
|
+
<SInput
|
|
75
|
+
v-model="newPlaylistName"
|
|
76
|
+
:placeholder="$t('playlist.new_playlist')"
|
|
77
|
+
:icon="newPlaylistName ? 'close' : undefined"
|
|
78
|
+
@icon-click="clearNewPlaylist"
|
|
79
|
+
/>
|
|
80
|
+
<SButton
|
|
81
|
+
size="s"
|
|
82
|
+
theme="primary"
|
|
83
|
+
:disabled="!newPlaylistName.trim()"
|
|
84
|
+
@click="createPlaylist"
|
|
85
|
+
>
|
|
86
|
+
{{ $t('create') }}
|
|
87
|
+
</SButton>
|
|
88
|
+
</div>
|
|
46
89
|
</div>
|
|
47
90
|
|
|
48
91
|
<template #footer>
|
|
@@ -51,7 +94,7 @@
|
|
|
51
94
|
wide
|
|
52
95
|
size="l"
|
|
53
96
|
:disabled="loadingUserPlaylists || loadingPostVideo"
|
|
54
|
-
theme="
|
|
97
|
+
theme="muted"
|
|
55
98
|
@click="closePopup"
|
|
56
99
|
>{{ $t('cancel') }}
|
|
57
100
|
</SButton>
|
|
@@ -69,31 +112,11 @@
|
|
|
69
112
|
</transition>
|
|
70
113
|
<!-- Add-->
|
|
71
114
|
|
|
72
|
-
<!-- New-->
|
|
73
|
-
<transition mode="out-in">
|
|
74
|
-
<SPopup
|
|
75
|
-
v-if="isPlaylistAdd && step === 'new'"
|
|
76
|
-
v-model="isPlaylistAdd"
|
|
77
|
-
sheet
|
|
78
|
-
transparent-backdrop
|
|
79
|
-
back
|
|
80
|
-
@close="closePopup"
|
|
81
|
-
@back="backPopup"
|
|
82
|
-
>
|
|
83
|
-
<template #title>{{ $t('playlist.new_playlist') }}</template>
|
|
84
|
-
<SPlaylistNew
|
|
85
|
-
:loading-post-playlist="loadingPostPlaylist"
|
|
86
|
-
@close="closePopup"
|
|
87
|
-
@post="postPlaylist"
|
|
88
|
-
/>
|
|
89
|
-
<slot/>
|
|
90
|
-
</SPopup>
|
|
91
|
-
</transition>
|
|
92
|
-
<!-- New-->
|
|
93
115
|
</template>
|
|
94
116
|
|
|
95
117
|
<script setup lang="ts">
|
|
96
|
-
import type { IPlaylistVideoFormType,
|
|
118
|
+
import type { IPlaylistVideoFormType, IPlaylistShort, IPlaylistCard, PaginatedResponse } from '../../types';
|
|
119
|
+
import { EPlaylistType } from 'itube-specs/runtime';
|
|
97
120
|
|
|
98
121
|
const props = defineProps<{
|
|
99
122
|
loadingPostPlaylist: boolean
|
|
@@ -106,7 +129,6 @@ const route = useRoute();
|
|
|
106
129
|
|
|
107
130
|
const emit = defineEmits<{
|
|
108
131
|
(eventName: 'post-playlist', eventValue: IPlaylistShort): void
|
|
109
|
-
(eventName: 'back-to-add'): void
|
|
110
132
|
(eventName: 'execute-user-playlists'): void
|
|
111
133
|
(eventName: 'save-video-to-playlist', eventValue: {
|
|
112
134
|
form: IPlaylistVideoFormType
|
|
@@ -116,18 +138,32 @@ const emit = defineEmits<{
|
|
|
116
138
|
|
|
117
139
|
const { videoCard, closePlaylistAdd, isPlaylistAdd, selectValue, step } = usePlaylistAdd();
|
|
118
140
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
const { FAVORITES_KEY } = useFavorites();
|
|
142
|
+
|
|
143
|
+
const playlistsItems = computed(() => {
|
|
144
|
+
return props.userPlaylists?.items
|
|
145
|
+
?.filter((item: IPlaylistCard) => item.name !== FAVORITES_KEY)
|
|
146
|
+
.map((item: IPlaylistCard) => ({
|
|
147
|
+
title: item.name,
|
|
148
|
+
value: item.id,
|
|
149
|
+
videosCount: item.videosCount,
|
|
150
|
+
}))
|
|
124
151
|
})
|
|
125
152
|
|
|
126
153
|
const id = computed(() => String(route.params['playlistId']));
|
|
127
|
-
const activePlaylist = computed(() => playlistsItems.value?.find(item => item.value === selectValue.value)?.value);
|
|
128
|
-
const playlistId = computed(() => activePlaylist.value ? [activePlaylist.value] : id.value);
|
|
129
154
|
|
|
130
|
-
|
|
155
|
+
function togglePlaylist(value: string) {
|
|
156
|
+
const idx = selectValue.value.indexOf(value);
|
|
157
|
+
if (idx === -1) {
|
|
158
|
+
selectValue.value.push(value);
|
|
159
|
+
} else {
|
|
160
|
+
selectValue.value.splice(idx, 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
watch(isPlaylistAdd, (val) => {
|
|
165
|
+
if (val) emit('execute-user-playlists');
|
|
166
|
+
}, { immediate: true });
|
|
131
167
|
|
|
132
168
|
async function closePopup() {
|
|
133
169
|
step.value = 'add';
|
|
@@ -135,16 +171,26 @@ async function closePopup() {
|
|
|
135
171
|
closePlaylistAdd();
|
|
136
172
|
}
|
|
137
173
|
|
|
138
|
-
|
|
139
|
-
|
|
174
|
+
const newPlaylistName = ref('');
|
|
175
|
+
const isPrivate = ref(false);
|
|
176
|
+
|
|
177
|
+
function clearNewPlaylist() {
|
|
178
|
+
newPlaylistName.value = '';
|
|
140
179
|
}
|
|
141
180
|
|
|
142
|
-
|
|
181
|
+
function createPlaylist() {
|
|
182
|
+
if (!newPlaylistName.value.trim()) return;
|
|
183
|
+
emit('post-playlist', {
|
|
184
|
+
name: newPlaylistName.value.trim(),
|
|
185
|
+
type: isPrivate.value ? EPlaylistType.Private : EPlaylistType.Public,
|
|
186
|
+
});
|
|
187
|
+
newPlaylistName.value = '';
|
|
188
|
+
isPrivate.value = false;
|
|
143
189
|
step.value = 'add';
|
|
144
190
|
}
|
|
145
191
|
|
|
146
192
|
const postVideoForm = computed(() => ({
|
|
147
|
-
playlistId:
|
|
193
|
+
playlistId: selectValue.value.length ? selectValue.value : undefined,
|
|
148
194
|
videoId: videoCard.value?.id ?? '',
|
|
149
195
|
VideoGUID: videoCard.value?.guid ?? '',
|
|
150
196
|
videoMd5: videoCard.value?.md5 ?? '',
|
|
@@ -159,7 +205,7 @@ async function onSaveClick() {
|
|
|
159
205
|
try {
|
|
160
206
|
emit('save-video-to-playlist', {
|
|
161
207
|
form: postVideoForm.value as IPlaylistVideoFormType,
|
|
162
|
-
id: String(
|
|
208
|
+
id: String(selectValue.value)
|
|
163
209
|
})
|
|
164
210
|
closePopup();
|
|
165
211
|
snackbarTheme.value = 'success';
|
|
@@ -174,15 +220,9 @@ watch(
|
|
|
174
220
|
playlistsItems,
|
|
175
221
|
(items) => {
|
|
176
222
|
if (!items?.length) return;
|
|
223
|
+
if (selectValue.value.length) return;
|
|
177
224
|
|
|
178
|
-
|
|
179
|
-
const fromRoute = items.find(item => item.value === routeId);
|
|
180
|
-
if (fromRoute) {
|
|
181
|
-
selectValue.value = fromRoute.value;
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
selectValue.value = items[0].value;
|
|
225
|
+
selectValue.value = ['favorites'];
|
|
186
226
|
},
|
|
187
227
|
{ immediate: true }
|
|
188
228
|
);
|
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
{{ title }}
|
|
11
11
|
</span>
|
|
12
12
|
<p class="s-slider__subtitle">
|
|
13
|
-
{{
|
|
14
|
-
<span class="s-slider__range">{{ (between && between[0]) || $attrs.min }}‑{{ (between && between[1]) || $attrs.max}}</span>
|
|
13
|
+
<span class="s-slider__range">{{ Array.isArray(displayValue) ? `${displayValue[0]}–${displayValue[1]}` : displayValue }}</span>
|
|
15
14
|
</p>
|
|
16
15
|
</div>
|
|
17
16
|
<div class="s-slider__wrapper">
|
|
18
17
|
<Slider
|
|
19
18
|
v-model="model"
|
|
20
19
|
v-bind="$attrs"
|
|
20
|
+
:tooltips="false"
|
|
21
|
+
@slide="(val: number | number[]) => displayValue = val"
|
|
21
22
|
/>
|
|
22
23
|
</div>
|
|
23
24
|
</div>
|
|
@@ -26,8 +27,10 @@
|
|
|
26
27
|
<script setup lang="ts">
|
|
27
28
|
import Slider from '@vueform/slider';
|
|
28
29
|
|
|
29
|
-
const {t} = useI18n();
|
|
30
30
|
const model = defineModel<number | number[]>();
|
|
31
|
+
const displayValue = ref<number | number[]>(model.value ?? 0)
|
|
32
|
+
|
|
33
|
+
watch(model, (val) => { if (val !== undefined) displayValue.value = val })
|
|
31
34
|
|
|
32
35
|
defineProps<{
|
|
33
36
|
title: string
|
|
@@ -3,23 +3,26 @@
|
|
|
3
3
|
class="s-snackbar"
|
|
4
4
|
:class="`--${snackbarTheme}`"
|
|
5
5
|
>
|
|
6
|
-
<
|
|
6
|
+
<div class="s-snackbar__icon-wrapper">
|
|
7
|
+
<SIcon class="s-snackbar__icon" :name="snackbarIcon" size="16"/>
|
|
8
|
+
</div>
|
|
7
9
|
{{
|
|
8
10
|
te(`snackbar.${convertSnackKey}`)
|
|
9
11
|
? t(`snackbar.${convertSnackKey}`)
|
|
10
12
|
: t('snackbar.something_went_wrong')
|
|
11
13
|
}}
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
<button
|
|
15
|
+
class="s-snackbar__close"
|
|
16
|
+
type="button"
|
|
15
17
|
@click="close"
|
|
16
|
-
>
|
|
17
|
-
|
|
18
|
+
>
|
|
19
|
+
<SIcon name="close" size="16" />
|
|
20
|
+
</button>
|
|
18
21
|
</div>
|
|
19
22
|
</template>
|
|
20
23
|
|
|
21
24
|
<script setup lang="ts">
|
|
22
|
-
import { convertString } from '
|
|
25
|
+
import { convertString } from 'itube-specs/runtime';
|
|
23
26
|
|
|
24
27
|
const { snackbarText, snackbarIcon, snackbarTheme, snackbarTimer } = useSnackbar();
|
|
25
28
|
|
|
@@ -13,20 +13,26 @@
|
|
|
13
13
|
|
|
14
14
|
<slot/>
|
|
15
15
|
|
|
16
|
+
<span class="s-timestamp__time _from-sm">
|
|
17
|
+
{{ time }}
|
|
18
|
+
</span>
|
|
19
|
+
|
|
16
20
|
<SIcon
|
|
17
21
|
class="s-timestamp__icon"
|
|
18
22
|
name="forward-step"
|
|
19
|
-
size="
|
|
23
|
+
size="14"
|
|
20
24
|
/>
|
|
21
25
|
</button>
|
|
22
26
|
</template>
|
|
23
27
|
|
|
24
28
|
<script setup lang="ts">
|
|
25
|
-
import type { IVttItem } from
|
|
29
|
+
import type { IVttItem } from 'itube-specs/runtime/utils/vtt-helper';
|
|
30
|
+
import { formatTime } from 'itube-specs/runtime';
|
|
26
31
|
|
|
27
32
|
const props = defineProps<{
|
|
28
33
|
pictureUrl: string,
|
|
29
34
|
coords: IVttItem['coords'];
|
|
35
|
+
duration: number;
|
|
30
36
|
}>();
|
|
31
37
|
|
|
32
38
|
const picture = `url(${props.pictureUrl})`;
|
|
@@ -38,6 +44,8 @@ const emit = defineEmits<{
|
|
|
38
44
|
(eventName: 'click'): void
|
|
39
45
|
}>();
|
|
40
46
|
|
|
47
|
+
const time = computed(() => formatTime(props.duration))
|
|
48
|
+
|
|
41
49
|
function onButtonClick() {
|
|
42
50
|
emit('click')
|
|
43
51
|
}
|
|
@@ -4,19 +4,8 @@ export function useApiFetcher<T>(
|
|
|
4
4
|
...apiArgs: any[]
|
|
5
5
|
) {
|
|
6
6
|
return async function fetchData() {
|
|
7
|
-
const hasKey = key?.trim();
|
|
8
|
-
const state = hasKey ? useState<T | null>(key!, () => null) : null;
|
|
9
|
-
|
|
10
|
-
if (state && state.value !== null && state.value !== undefined) {
|
|
11
|
-
return state.value;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (state) state.value = result;
|
|
18
|
-
|
|
19
|
-
return result;
|
|
8
|
+
return await apiMethod(...apiArgs);
|
|
20
9
|
} catch (err) {
|
|
21
10
|
throw createError({
|
|
22
11
|
statusCode: 500,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const FAVORITES_KEY = 'favorites';
|
|
2
|
+
|
|
3
|
+
export const useFavorites = () => {
|
|
4
|
+
const favoritesId = useState<string | null>('favorites-playlist-id', () => null);
|
|
5
|
+
const favoritesFirstVideoId = useState<string | null>('favorites-first-video-id', () => null);
|
|
6
|
+
|
|
7
|
+
const favoritesLink = computed(() =>
|
|
8
|
+
favoritesId.value && favoritesFirstVideoId.value
|
|
9
|
+
? `/playlists/${favoritesId.value}/${favoritesFirstVideoId.value}`
|
|
10
|
+
: null
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
FAVORITES_KEY,
|
|
15
|
+
favoritesId,
|
|
16
|
+
favoritesFirstVideoId,
|
|
17
|
+
favoritesLink,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FeatureFlags } from '~/config/featureFlags.config';
|
|
2
|
+
|
|
3
|
+
export function useNavigationItems() {
|
|
4
|
+
const { favoritesLink } = useFavorites();
|
|
5
|
+
|
|
6
|
+
const navigationItems = computed(() => {
|
|
7
|
+
const favLink = favoritesLink.value ?? '';
|
|
8
|
+
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
title: 'videos',
|
|
12
|
+
link: '/',
|
|
13
|
+
icon: 'video-camera',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
title: 'categories',
|
|
17
|
+
link: '/categories',
|
|
18
|
+
icon: 'grid',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
title: 'channels',
|
|
22
|
+
link: '/channels',
|
|
23
|
+
icon: 'play-circle',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'gay_models',
|
|
27
|
+
link: '/models',
|
|
28
|
+
icon: 'user',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'playlists',
|
|
32
|
+
link: '/playlists',
|
|
33
|
+
icon: 'list',
|
|
34
|
+
},
|
|
35
|
+
...(FeatureFlags.FeatureLiveEnabled ? [{
|
|
36
|
+
title: 'live_sex',
|
|
37
|
+
link: '/2257',
|
|
38
|
+
icon: 'photo-cam',
|
|
39
|
+
accent: true,
|
|
40
|
+
}] : []),
|
|
41
|
+
...(FeatureFlags.FeatureHistoryEnabled ? [{
|
|
42
|
+
title: 'history',
|
|
43
|
+
link: '/2257',
|
|
44
|
+
icon: 'time',
|
|
45
|
+
top: true,
|
|
46
|
+
}] : []),
|
|
47
|
+
...(FeatureFlags.FeatureFavoritesEnabled ? [{
|
|
48
|
+
title: 'favorites',
|
|
49
|
+
link: favLink,
|
|
50
|
+
icon: 'heart',
|
|
51
|
+
top: true,
|
|
52
|
+
childs: [
|
|
53
|
+
{
|
|
54
|
+
title: 'videos',
|
|
55
|
+
icon: 'video-camera',
|
|
56
|
+
link: favLink,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: 'watch_later',
|
|
60
|
+
icon: 'time',
|
|
61
|
+
link: '/2257',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: 'playlists',
|
|
65
|
+
icon: 'list',
|
|
66
|
+
link: '/2257',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: 'models',
|
|
70
|
+
icon: 'user',
|
|
71
|
+
link: '/2257',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: 'channels',
|
|
75
|
+
icon: 'play-circle',
|
|
76
|
+
link: '/2257',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}] : []),
|
|
80
|
+
];
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return { navigationItems };
|
|
84
|
+
}
|