itube-specs 0.0.759 → 0.0.760
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/grids/s-grid-categories.vue +2 -2
- package/components/grids/s-grid-channels.vue +2 -2
- package/components/grids/s-grid-models.vue +2 -2
- package/components/grids/s-grid-playlists.vue +2 -2
- package/components/grids/s-grid-videos.vue +23 -30
- package/components/page-components/s-filter-page.vue +2 -9
- package/components/page-components/s-filter.vue +2 -2
- package/components/page-components/s-info-grid.vue +4 -4
- package/components/page-components/s-report.vue +13 -3
- package/components/page-components/s-share.vue +24 -1
- package/composables/use-meta.ts +15 -7
- package/composables/use-model-filter-chips.ts +50 -46
- package/package.json +1 -1
- package/components/playlist/s-playlist-like.vue.unused +0 -96
- package/components/playlist/s-playlist-new.vue.unused +0 -57
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="s-grid-categories">
|
|
3
|
-
<
|
|
3
|
+
<SCategoryCard
|
|
4
4
|
v-for="(item, index) in categories"
|
|
5
5
|
:key="item.guid"
|
|
6
6
|
:card="item"
|
|
@@ -16,5 +16,5 @@ import type { ICategoryCard } from '../../types';
|
|
|
16
16
|
defineProps<{
|
|
17
17
|
categories: Array<ICategoryCard>,
|
|
18
18
|
priority?: boolean
|
|
19
|
-
}>()
|
|
19
|
+
}>();
|
|
20
20
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
id="anchor"
|
|
4
4
|
class="s-grid-channels"
|
|
5
5
|
>
|
|
6
|
-
<
|
|
6
|
+
<SChannelCard
|
|
7
7
|
v-for="(item, index) in items"
|
|
8
8
|
:key="item.guid"
|
|
9
9
|
class="s-grid-channels__card"
|
|
@@ -19,5 +19,5 @@ import type { IChannelCard } from '../../types';
|
|
|
19
19
|
defineProps<{
|
|
20
20
|
items: IChannelCard[]
|
|
21
21
|
priority?: boolean
|
|
22
|
-
}>()
|
|
22
|
+
}>();
|
|
23
23
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class="s-grid-models"
|
|
4
4
|
:class="{'--footer': footer}"
|
|
5
5
|
>
|
|
6
|
-
<
|
|
6
|
+
<SModelCard
|
|
7
7
|
v-for="(item, index) in items"
|
|
8
8
|
:key="`model-card-${item.guid}`"
|
|
9
9
|
class="s-grid-models__item"
|
|
@@ -21,5 +21,5 @@ defineProps<{
|
|
|
21
21
|
items: IModelCard[]
|
|
22
22
|
footer?: boolean
|
|
23
23
|
priority?: boolean
|
|
24
|
-
}>()
|
|
24
|
+
}>();
|
|
25
25
|
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="s-grid-playlists">
|
|
3
|
-
<
|
|
3
|
+
<SPlaylistCard
|
|
4
4
|
v-for="(item, index) in items"
|
|
5
5
|
:key="`user-playlist-${index}`"
|
|
6
6
|
:card="item"
|
|
@@ -17,5 +17,5 @@ defineProps<{
|
|
|
17
17
|
items: IPlaylistCard[]
|
|
18
18
|
topPlaylists?: boolean
|
|
19
19
|
priority?: boolean
|
|
20
|
-
}>()
|
|
20
|
+
}>();
|
|
21
21
|
</script>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
]"
|
|
7
7
|
>
|
|
8
8
|
<slot name="grid-start"/>
|
|
9
|
-
<
|
|
9
|
+
<SVideoCard
|
|
10
10
|
v-for="(item, index) in eagerItems"
|
|
11
11
|
:key="`video-${item.guid}`"
|
|
12
12
|
class="s-grid-videos__card"
|
|
@@ -18,34 +18,28 @@
|
|
|
18
18
|
:playlist="playlist"
|
|
19
19
|
/>
|
|
20
20
|
<slot/>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
:top-chips="topChips"
|
|
44
|
-
:playlist-id="playlistId"
|
|
45
|
-
:playlist="playlist"
|
|
46
|
-
/>
|
|
47
|
-
</div>
|
|
48
|
-
</NuxtLazyHydrate>
|
|
21
|
+
<LazySVideoCard
|
|
22
|
+
v-for="item in idleItems"
|
|
23
|
+
:key="`video-idle-${item.guid}`"
|
|
24
|
+
class="s-grid-videos__card"
|
|
25
|
+
:card="item"
|
|
26
|
+
loading="lazy"
|
|
27
|
+
:top-chips="topChips"
|
|
28
|
+
:playlist-id="playlistId"
|
|
29
|
+
:playlist="playlist"
|
|
30
|
+
hydrate-on-idle
|
|
31
|
+
/>
|
|
32
|
+
<LazySVideoCard
|
|
33
|
+
v-for="item in lazyItems"
|
|
34
|
+
:key="`video-lazy-${item.guid}`"
|
|
35
|
+
class="s-grid-videos__card"
|
|
36
|
+
:card="item"
|
|
37
|
+
loading="lazy"
|
|
38
|
+
:top-chips="topChips"
|
|
39
|
+
:playlist-id="playlistId"
|
|
40
|
+
:playlist="playlist"
|
|
41
|
+
hydrate-on-visible
|
|
42
|
+
/>
|
|
49
43
|
<slot name="grid-end"/>
|
|
50
44
|
</div>
|
|
51
45
|
</template>
|
|
@@ -65,7 +59,6 @@ const props = defineProps<{
|
|
|
65
59
|
topChips?: string[]
|
|
66
60
|
}>();
|
|
67
61
|
|
|
68
|
-
const route = useRoute();
|
|
69
62
|
const isMobile = useState<boolean>('isMobile');
|
|
70
63
|
const eagerCount = computed(() => isMobile.value ? 4 : 12);
|
|
71
64
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<span class="s-filter-page__group-title">
|
|
20
20
|
{{ group?.title }}
|
|
21
21
|
</span>
|
|
22
|
-
<
|
|
22
|
+
<SSegmentedControl
|
|
23
23
|
v-if="group?.name === 'physical'"
|
|
24
24
|
:items="unitItems"
|
|
25
25
|
:model-value="units"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
:label="item.title"
|
|
62
62
|
@update:model-value="val => updateFilter(item.name, val)"
|
|
63
63
|
/>
|
|
64
|
-
<
|
|
64
|
+
<SSegmentedControl
|
|
65
65
|
v-if="(item.kind === 'radio') && (item.group.name === group?.name)"
|
|
66
66
|
class="s-filter-page__radio"
|
|
67
67
|
:items="getRadioItems(item.options)"
|
|
@@ -69,13 +69,6 @@
|
|
|
69
69
|
:model-value="getValue(item.name)"
|
|
70
70
|
@update:model-value="val => updateFilter(item.name, val)"
|
|
71
71
|
/>
|
|
72
|
-
<FFilterByChips
|
|
73
|
-
v-if="(item.kind === 'chips') && (item.group.name === group?.name)"
|
|
74
|
-
class="f-filters-main__chips"
|
|
75
|
-
:items="item.options"
|
|
76
|
-
:title="item.title"
|
|
77
|
-
:filter-name="item.name"
|
|
78
|
-
/>
|
|
79
72
|
</template>
|
|
80
73
|
</div>
|
|
81
74
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/>
|
|
9
9
|
|
|
10
10
|
<transition>
|
|
11
|
-
<
|
|
11
|
+
<LazySFilterPopup
|
|
12
12
|
v-if="filterOpen"
|
|
13
13
|
v-model="filterOpen"
|
|
14
14
|
:title="$t('filter')"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
</SButton>
|
|
40
40
|
</div>
|
|
41
41
|
</template>
|
|
42
|
-
</
|
|
42
|
+
</LazySFilterPopup>
|
|
43
43
|
</transition>
|
|
44
44
|
</div>
|
|
45
45
|
</template>
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
]"
|
|
33
33
|
>
|
|
34
34
|
<img
|
|
35
|
-
v-if="item.name === '
|
|
35
|
+
v-if="item.name === 'nationality_iso_codes' && isoCodes"
|
|
36
36
|
class="s-info-grid__item-flag"
|
|
37
|
-
:src="`/icons/flags/${
|
|
37
|
+
:src="`/icons/flags/${isoCodes[vIndex]}.svg`"
|
|
38
38
|
width="20"
|
|
39
39
|
height="14"
|
|
40
|
-
:alt="
|
|
40
|
+
:alt="isoCodes[vIndex]"
|
|
41
41
|
/>
|
|
42
42
|
<SIcon v-else-if="item.icon" class="s-info-grid__item-icon" :name="item.icon" prefix="models" size="16"/>
|
|
43
43
|
{{ value.title }}
|
|
@@ -54,7 +54,7 @@ import type { IGroupedParameter, IGroupedParameterItem } from '../../types';
|
|
|
54
54
|
|
|
55
55
|
defineProps<{
|
|
56
56
|
groups: IGroupedParameter[]
|
|
57
|
-
|
|
57
|
+
isoCodes?: string
|
|
58
58
|
}>();
|
|
59
59
|
|
|
60
60
|
const PLAIN_GROUP_ITEM_NAMES = ['turnons'];
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
<p
|
|
47
47
|
v-if="activeCategory.text"
|
|
48
48
|
class="s-report__form-text"
|
|
49
|
+
:class="{'--error': errorRadio && !hasReasonHeading}"
|
|
49
50
|
>{{ t(`report_form.${activeCategory.text}`) }}
|
|
50
51
|
</p>
|
|
51
52
|
<p
|
|
@@ -70,6 +71,12 @@
|
|
|
70
71
|
:key="`${subItem.label}-${subIndex}`"
|
|
71
72
|
:class="[{'--wide': subItem.wide},subItem.marginClass]"
|
|
72
73
|
>
|
|
74
|
+
<p
|
|
75
|
+
v-if="subItem.type === 'heading'"
|
|
76
|
+
class="s-report__form-text"
|
|
77
|
+
:class="{'--error': errorRadio}"
|
|
78
|
+
>{{ t(`report_form.${subItem.text}`) }}
|
|
79
|
+
</p>
|
|
73
80
|
<SInput
|
|
74
81
|
v-if="['text', 'tel', 'textarea'].includes(subItem.type)"
|
|
75
82
|
v-model="form.data[subItem.value] as string"
|
|
@@ -96,8 +103,6 @@
|
|
|
96
103
|
:value="subItem.value"
|
|
97
104
|
:label="t(`report_form.${subItem.text}`)"
|
|
98
105
|
:required="subItem.required"
|
|
99
|
-
:error="errorRadio"
|
|
100
|
-
@update:error="(val: boolean) => errorRadio = val"
|
|
101
106
|
/>
|
|
102
107
|
</div>
|
|
103
108
|
</form>
|
|
@@ -122,7 +127,7 @@
|
|
|
122
127
|
</template>
|
|
123
128
|
|
|
124
129
|
<script setup lang="ts">
|
|
125
|
-
import { reportFormsScheme } from '../../lib
|
|
130
|
+
import { reportFormsScheme } from '../../lib';
|
|
126
131
|
import { EReportFormsSubjects, validateEmail, validatePhone } from '../../runtime';
|
|
127
132
|
import type { InputTypes, IReportForm, IReportRequest } from '../../types';
|
|
128
133
|
|
|
@@ -172,12 +177,17 @@ watch(isReportPopupOpen, (val) => {
|
|
|
172
177
|
}
|
|
173
178
|
});
|
|
174
179
|
|
|
180
|
+
watch(reasonValue, (val) => {
|
|
181
|
+
if (val) errorRadio.value = false;
|
|
182
|
+
});
|
|
183
|
+
|
|
175
184
|
const videoGuid = computed(() => reportedVideoCard.value.guid);
|
|
176
185
|
|
|
177
186
|
const loading = ref(false);
|
|
178
187
|
|
|
179
188
|
const activeCategory = computed(() => reportFormsScheme.find((subItem) => subItem.subject === activeStep.value))
|
|
180
189
|
const requiredFields = computed(() => activeCategory.value?.items?.filter(item => item.required).map(item => item.value));
|
|
190
|
+
const hasReasonHeading = computed(() => activeCategory.value?.items?.some(item => item.type === 'heading'));
|
|
181
191
|
|
|
182
192
|
const { showSuccess, showError, resetSnackbar } = useSnackbar();
|
|
183
193
|
|
|
@@ -81,6 +81,29 @@ const fullUrl = computed(() =>
|
|
|
81
81
|
props.url ? origin.value + props.url : origin.value + route.fullPath
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
+
const isMobile = useState<boolean>('isMobile');
|
|
85
|
+
|
|
86
|
+
function canNativeShare() {
|
|
87
|
+
return typeof navigator !== 'undefined' && !!navigator.share && isMobile.value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function nativeShare() {
|
|
91
|
+
navigator.share({
|
|
92
|
+
title: document.title,
|
|
93
|
+
text: document.title,
|
|
94
|
+
url: fullUrl.value,
|
|
95
|
+
}).catch((err) => {
|
|
96
|
+
console.warn('Share failed:', err);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
watch(() => props.modelValue, (val) => {
|
|
101
|
+
if (val && canNativeShare()) {
|
|
102
|
+
emit('update:modelValue', false);
|
|
103
|
+
nativeShare();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
84
107
|
function copyUrl() {
|
|
85
108
|
if (!process.client) return;
|
|
86
109
|
navigator.clipboard.writeText(fullUrl.value).then(() => {
|
|
@@ -119,4 +142,4 @@ const buttons = computed(() => {
|
|
|
119
142
|
},
|
|
120
143
|
]
|
|
121
144
|
})
|
|
122
|
-
</script>
|
|
145
|
+
</script>
|
package/composables/use-meta.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { IChipsItem } from '../types';
|
|
|
14
14
|
* @param thirdText - третий подставляемый текст
|
|
15
15
|
* @param fourthText - четвёртый подставляемый текст
|
|
16
16
|
*/
|
|
17
|
-
export function useMeta(t, page: string, brandName: string, sortOptions?: IChipsItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>) {
|
|
17
|
+
export function useMeta(t: (key: string, params?: Record<string, unknown>) => string, page: string, brandName: string, sortOptions?: IChipsItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>) {
|
|
18
18
|
const route = useRoute();
|
|
19
19
|
|
|
20
20
|
const sortType = computed(() => {
|
|
@@ -48,12 +48,20 @@ export function useMeta(t, page: string, brandName: string, sortOptions?: IChips
|
|
|
48
48
|
const metaTitle = computed(() => getPath('title'));
|
|
49
49
|
const h1 = computed(() => getPath('h1'));
|
|
50
50
|
|
|
51
|
-
const meta = computed(() =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const meta = computed(() => {
|
|
52
|
+
const description = getPath('meta_description');
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
title: metaTitle.value,
|
|
56
|
+
meta: [
|
|
57
|
+
{ name: 'description', content: description },
|
|
58
|
+
{ property: 'og:title', content: metaTitle.value },
|
|
59
|
+
{ property: 'og:description', content: description },
|
|
60
|
+
{ name: 'twitter:title', content: metaTitle.value },
|
|
61
|
+
{ name: 'twitter:description', content: description },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
});
|
|
57
65
|
|
|
58
66
|
return {
|
|
59
67
|
meta,
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import type { IChipsItem, IModelFilter, IModelFilterOptions } from '../types'
|
|
2
|
-
import type { LocationQuery } from '#vue-router'
|
|
3
|
-
import { getMonth } from '../runtime'
|
|
1
|
+
import type { IChipsItem, IModelFilter, IModelFilterOptions } from '../types';
|
|
2
|
+
import type { LocationQuery } from '#vue-router';
|
|
3
|
+
import { getMonth } from '../runtime';
|
|
4
4
|
import type { Ref } from 'vue';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Формирует вычисляемый список чипсов из активных filter_* параметров в query маршрута.
|
|
8
|
-
* @param filters - список схем фильтров модели
|
|
9
|
-
* @param route - текущий маршрут с query
|
|
10
|
-
* @param t - функция перевода (i18n)
|
|
11
|
-
*/
|
|
12
6
|
export function useFilterChipsItems(
|
|
13
7
|
filters: Ref<IModelFilter[]>,
|
|
14
8
|
route: { query: LocationQuery },
|
|
@@ -21,52 +15,58 @@ export function useFilterChipsItems(
|
|
|
21
15
|
item
|
|
22
16
|
.replace('filter_', '')
|
|
23
17
|
.replace(/_/g, ' ')
|
|
24
|
-
)
|
|
18
|
+
);
|
|
25
19
|
|
|
26
20
|
const groups = Object.values(
|
|
27
21
|
queryItems.reduce((acc: Record<string, string[]>, str: string) => {
|
|
28
|
-
const parts = str.split(' ')
|
|
29
|
-
const last = parts.at(-1)
|
|
22
|
+
const parts = str.split(' ');
|
|
23
|
+
const last = parts.at(-1);
|
|
30
24
|
|
|
31
25
|
const key =
|
|
32
26
|
last === 'from' || last === 'to'
|
|
33
27
|
? parts.slice(0, -1).join(' ')
|
|
34
|
-
: str
|
|
28
|
+
: str;
|
|
35
29
|
|
|
36
|
-
acc[key] ??= []
|
|
30
|
+
acc[key] ??= [];
|
|
37
31
|
|
|
38
|
-
if (last === 'from') acc[key].unshift(str)
|
|
39
|
-
else acc[key].push(str)
|
|
32
|
+
if (last === 'from') acc[key].unshift(str);
|
|
33
|
+
else acc[key].push(str);
|
|
40
34
|
|
|
41
|
-
return acc
|
|
35
|
+
return acc;
|
|
42
36
|
}, {})
|
|
43
|
-
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const findFilter = (items: string[]) => {
|
|
40
|
+
const key = items[0].replace(' from', '').replace(' to', '');
|
|
41
|
+
return filters.value.find(f => f.name.replace(/_/g, ' ') === key);
|
|
42
|
+
};
|
|
44
43
|
|
|
45
44
|
const getValue = (item: string, index: number) => {
|
|
46
45
|
const filter = filters.value.find(filter =>
|
|
47
46
|
filter.name ===
|
|
48
47
|
item
|
|
49
|
-
.replace(/ /g,
|
|
48
|
+
.replace(/ /g,
|
|
49
|
+
'_')
|
|
50
50
|
.replace('_from', '')
|
|
51
51
|
.replace('_to', '')
|
|
52
|
-
)
|
|
52
|
+
);
|
|
53
53
|
|
|
54
54
|
const defaultValue =
|
|
55
55
|
index === 0
|
|
56
56
|
? filter?.options[0]
|
|
57
|
-
: filter?.options.at(-1)
|
|
57
|
+
: filter?.options.at(-1);
|
|
58
58
|
|
|
59
|
-
const value = route.query[`filter_${item.replace(/ /g, '_')}`]
|
|
59
|
+
const value = route.query[`filter_${item.replace(/ /g, '_')}`];
|
|
60
60
|
|
|
61
61
|
if (item.includes('month')) {
|
|
62
62
|
return getMonth(
|
|
63
63
|
t,
|
|
64
64
|
Number(value ?? defaultValue?.name) - 1
|
|
65
|
-
)
|
|
65
|
+
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (!isNaN(Number(value))) {
|
|
69
|
-
return value
|
|
69
|
+
return value;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return (
|
|
@@ -75,31 +75,35 @@ export function useFilterChipsItems(
|
|
|
75
75
|
option.name === value
|
|
76
76
|
)?.title ??
|
|
77
77
|
defaultValue?.title
|
|
78
|
-
)
|
|
79
|
-
}
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
80
|
|
|
81
81
|
const getValueText = (items: string[]) =>
|
|
82
|
-
[...new Set(items.map(getValue))].join(' - ')
|
|
83
|
-
|
|
84
|
-
const chipsTitle = (items: string[]) => {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
[...new Set(items.map(getValue).filter(v => v !== undefined && v !== null && v !== ''))].join(' - ');
|
|
83
|
+
|
|
84
|
+
const chipsTitle = (items: string[], filter: IModelFilter) => {
|
|
85
|
+
const valueText = getValueText(items);
|
|
86
|
+
const text = `${filter.title}: ${valueText}`;
|
|
87
|
+
return text.length > 30 ? valueText : text;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return groups
|
|
91
|
+
.map(item => {
|
|
92
|
+
const filter = findFilter(item);
|
|
93
|
+
if (!filter) return null;
|
|
94
|
+
const valueText = getValueText(item);
|
|
95
|
+
if (!valueText) return null;
|
|
96
|
+
return {
|
|
97
|
+
title: chipsTitle(item, filter),
|
|
98
|
+
value: item.map(
|
|
99
|
+
sub => `filter_${sub.replace(/ /g, '_')}`
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
})
|
|
103
|
+
.filter((c): c is IChipsItem => c !== null);
|
|
104
|
+
});
|
|
101
105
|
|
|
102
106
|
return {
|
|
103
107
|
chipsItems,
|
|
104
|
-
}
|
|
108
|
+
};
|
|
105
109
|
}
|
package/package.json
CHANGED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="s-playlist-like">
|
|
3
|
-
<button
|
|
4
|
-
class="s-playlist-like__control"
|
|
5
|
-
:class="{'--active': isLiked}"
|
|
6
|
-
type="button"
|
|
7
|
-
:title="t('like')"
|
|
8
|
-
@click="onReactionClick('like')"
|
|
9
|
-
>
|
|
10
|
-
<SIcon class="s-playlist-like__control-icon" name="thumbs-up" size="16"/>
|
|
11
|
-
{{ formatNumber(Number(resultLikes)) }}
|
|
12
|
-
</button>
|
|
13
|
-
<div
|
|
14
|
-
v-if="bar"
|
|
15
|
-
class="s-playlist-like__bar"
|
|
16
|
-
>
|
|
17
|
-
<div
|
|
18
|
-
class="s-playlist-like__bar-likes"
|
|
19
|
-
:style="{ width: likePercent + '%' }"
|
|
20
|
-
/>
|
|
21
|
-
<div
|
|
22
|
-
class="s-playlist-like__bar-dislikes"
|
|
23
|
-
:style="{ width: dislikePercent + '%' }"
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
26
|
-
<button
|
|
27
|
-
class="s-playlist-like__control"
|
|
28
|
-
:class="{'--active': isDisliked}"
|
|
29
|
-
type="button"
|
|
30
|
-
:title="t('dislike')"
|
|
31
|
-
@click="onReactionClick('dislike')"
|
|
32
|
-
>
|
|
33
|
-
<SIcon class="s-playlist-like__control-icon" name="thumbs-down" size="16"/>
|
|
34
|
-
{{ formatNumber(Number(resultDislikes)) }}
|
|
35
|
-
</button>
|
|
36
|
-
</div>
|
|
37
|
-
</template>
|
|
38
|
-
|
|
39
|
-
<script setup lang="ts">
|
|
40
|
-
import { formatNumber } from '../../runtime';
|
|
41
|
-
|
|
42
|
-
const { t } = useI18n();
|
|
43
|
-
|
|
44
|
-
const props = defineProps<{
|
|
45
|
-
playlistId: string
|
|
46
|
-
likes: number
|
|
47
|
-
dislikes: number
|
|
48
|
-
bar?: boolean
|
|
49
|
-
}>()
|
|
50
|
-
|
|
51
|
-
const emit = defineEmits<{
|
|
52
|
-
(eventName: 'like', eventValue: string): void
|
|
53
|
-
(eventName: 'dislike', eventValue: string): void
|
|
54
|
-
}>()
|
|
55
|
-
|
|
56
|
-
const likeStatus = ref<'like' | 'dislike' | null>(null);
|
|
57
|
-
const isLiked = computed(() => likeStatus.value === 'like');
|
|
58
|
-
const isDisliked = computed(() => likeStatus.value === 'dislike');
|
|
59
|
-
const resultLikes = ref(props.likes);
|
|
60
|
-
const resultDislikes = ref(props.dislikes);
|
|
61
|
-
|
|
62
|
-
onMounted(() => {
|
|
63
|
-
const stored = localStorage.getItem(props.playlistId);
|
|
64
|
-
if (stored === 'like' || stored === 'dislike') {
|
|
65
|
-
likeStatus.value = stored;
|
|
66
|
-
if (stored === 'like') resultLikes.value += 1;
|
|
67
|
-
if (stored === 'dislike') resultDislikes.value += 1;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
async function onReactionClick(type: 'like' | 'dislike') {
|
|
72
|
-
if (likeStatus.value !== type) {
|
|
73
|
-
likeStatus.value = type;
|
|
74
|
-
localStorage.setItem(props.playlistId, type);
|
|
75
|
-
|
|
76
|
-
const isLike = type === 'like';
|
|
77
|
-
|
|
78
|
-
if (isLike) {
|
|
79
|
-
if (resultLikes.value < 1000) resultLikes.value += 1;
|
|
80
|
-
if (resultDislikes.value > 0) resultDislikes.value -= 1;
|
|
81
|
-
emit('like', props.playlistId);
|
|
82
|
-
} else {
|
|
83
|
-
if (resultDislikes.value < 1000) resultDislikes.value += 1;
|
|
84
|
-
if (resultLikes.value > 0) resultLikes.value -= 1;
|
|
85
|
-
emit('dislike', props.playlistId);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const likes = computed(() => resultLikes.value || 0);
|
|
91
|
-
const dislikes = computed(() => resultDislikes.value || 0);
|
|
92
|
-
const total = computed(() => likes.value + dislikes.value || 1);
|
|
93
|
-
|
|
94
|
-
const likePercent = computed(() => (likes.value / total.value) * 100);
|
|
95
|
-
const dislikePercent = computed(() => (dislikes.value / total.value) * 100);
|
|
96
|
-
</script>
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="s-playlist-new" :class="{'_loading': loadingPostPlaylist}">
|
|
3
|
-
<SInput
|
|
4
|
-
v-model="newTitleValue"
|
|
5
|
-
:label="$t('title')"
|
|
6
|
-
/>
|
|
7
|
-
<SPlaylistPrivateToggle v-model="isPrivate" />
|
|
8
|
-
<div class="s-playlist-new__buttons">
|
|
9
|
-
<SButton
|
|
10
|
-
wide
|
|
11
|
-
theme="secondary"
|
|
12
|
-
@click="closePopup"
|
|
13
|
-
>{{ $t('cancel') }}
|
|
14
|
-
</SButton>
|
|
15
|
-
<SButton
|
|
16
|
-
wide
|
|
17
|
-
theme="primary"
|
|
18
|
-
@click="onSaveClick"
|
|
19
|
-
>{{ $t('save') }}
|
|
20
|
-
</SButton>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<script setup lang="ts">
|
|
26
|
-
import { EPlaylistType } from '../../runtime';
|
|
27
|
-
import type { IPlaylistShort } from '../../types';
|
|
28
|
-
|
|
29
|
-
defineProps<{
|
|
30
|
-
loadingPostPlaylist: boolean
|
|
31
|
-
}>()
|
|
32
|
-
|
|
33
|
-
const emit = defineEmits<{
|
|
34
|
-
(eventName: 'close'): void
|
|
35
|
-
(eventName: 'post', eventValue: IPlaylistShort): void
|
|
36
|
-
}>()
|
|
37
|
-
|
|
38
|
-
const { closePlaylistAdd } = usePlaylistAdd();
|
|
39
|
-
const newTitleValue = ref('');
|
|
40
|
-
const isPrivate = ref(false);
|
|
41
|
-
|
|
42
|
-
function closePopup() {
|
|
43
|
-
closePlaylistAdd();
|
|
44
|
-
emit('close');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function onSaveClick() {
|
|
48
|
-
try {
|
|
49
|
-
emit('post', {
|
|
50
|
-
name: newTitleValue.value,
|
|
51
|
-
type: isPrivate.value ? EPlaylistType.Private : EPlaylistType.Public
|
|
52
|
-
});
|
|
53
|
-
} catch(error) {
|
|
54
|
-
console.log(error, 'error create playlist');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
</script>
|