itube-specs 0.0.648 → 0.0.651
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/auth/s-auth-recovery.vue +2 -1
- package/components/auth/s-auth-register.vue +7 -3
- package/components/cards/s-video-mini-card.vue +2 -2
- package/components/page-components/s-filter-page.vue +0 -1
- package/components/page-components/s-filter-popup.vue +4 -0
- package/components/page-components/s-like.vue +1 -1
- package/components/page-components/s-report.vue +7 -9
- package/components/page-components/s-share.vue +31 -40
- package/components/playlist/s-playlist-add.vue +134 -83
- package/components/playlist/s-playlist-delete-video.vue +5 -5
- package/components/playlist/s-playlist-input.vue +88 -0
- package/components/ui/s-avatar.vue +33 -0
- package/components/ui/s-button.vue +3 -1
- package/components/ui/s-dropdown.vue +17 -4
- package/components/ui/s-link.vue +3 -1
- package/components/ui/s-select.vue +16 -13
- package/components/ui/s-toggle.vue +3 -2
- package/components/ui/s-tooltip.vue +57 -0
- package/composables/use-fetch-models-by-phrases.ts +1 -1
- package/composables/use-filter-scheme.ts +2 -1
- package/composables/use-global-scroll-lock.ts +24 -16
- package/package.json +1 -1
- package/types/button-themes.d.ts +1 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
:label="$t('email')"
|
|
16
16
|
:placeholder="$t('email')"
|
|
17
17
|
hide-label
|
|
18
|
+
inputmode="email"
|
|
18
19
|
autocomplete="email"
|
|
19
20
|
:error="error.email"
|
|
20
21
|
@update:error="(val: boolean) => error.email = val"
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
|
|
41
42
|
<script setup lang="ts">
|
|
42
43
|
import type { IPasswordForm } from '../../types';
|
|
43
|
-
import { validateEmail } from '
|
|
44
|
+
import { validateEmail } from 'itube-specs/runtime';
|
|
44
45
|
import { AuthorizationApiService } from '~/services/api/authorization.service';
|
|
45
46
|
|
|
46
47
|
const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
:placeholder="t('email')"
|
|
16
16
|
:label="t('email')"
|
|
17
17
|
hide-label
|
|
18
|
+
inputmode="email"
|
|
18
19
|
autocomplete="email"
|
|
19
20
|
:error="error.email"
|
|
20
21
|
@update:error="(val: boolean) => error.email = val"
|
|
@@ -80,8 +81,9 @@
|
|
|
80
81
|
|
|
81
82
|
<script setup lang="ts">
|
|
82
83
|
import type { IRegistrateForm } from '../../types';
|
|
83
|
-
import { validateEmail, validatePassword, validateUsername } from '
|
|
84
|
+
import { EPlaylistType, validateEmail, validatePassword, validateUsername } from 'itube-specs/runtime';
|
|
84
85
|
import { AuthorizationApiService } from '~/services/api/authorization.service';
|
|
86
|
+
import { PlaylistsApiService } from '~/services/api/playlists.service';
|
|
85
87
|
|
|
86
88
|
const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
|
|
87
89
|
|
|
@@ -156,11 +158,13 @@ async function submit() {
|
|
|
156
158
|
|
|
157
159
|
await useUser(AuthorizationApiService).register(form.value);
|
|
158
160
|
|
|
161
|
+
// Создаем сразу Favorites плейлист, возможно костыль убрать когда будет нормальная логика с беком
|
|
162
|
+
// const { FAVORITES_KEY } = useFavorites();
|
|
163
|
+
// await PlaylistsApiService.postPlaylist(FAVORITES_KEY, EPlaylistType.Private);
|
|
164
|
+
|
|
159
165
|
useAuthPopup().closeAuthPopup();
|
|
160
166
|
snackbarTheme.value = 'success';
|
|
161
167
|
snackbarText.value = 'Registration success';
|
|
162
|
-
|
|
163
|
-
// ... успех
|
|
164
168
|
} catch (err) {
|
|
165
169
|
setErrorState(err);
|
|
166
170
|
console.error('Ошибка reCAPTCHA или регистрации:', err);
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
</template>
|
|
26
26
|
|
|
27
27
|
<script setup lang="ts">
|
|
28
|
-
import type { IVideoCard } from '
|
|
29
|
-
import { getDuration } from '
|
|
28
|
+
import type { IVideoCard } from 'itube-specs/types';
|
|
29
|
+
import { getDuration } from 'itube-specs/runtime';
|
|
30
30
|
|
|
31
31
|
const props = defineProps<{
|
|
32
32
|
card: IVideoCard
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<span class="s-like__control-icon-wrapper">
|
|
36
36
|
<SIcon class="s-like__control-icon --dislike" name="thumbs-down" size="16"/>
|
|
37
37
|
</span>
|
|
38
|
-
<span class="s-like__control-text">{{ formatNumber(Number(resultDislikes)) }}</span>
|
|
38
|
+
<span class="s-like__control-text">{{ formatNumber(Math.abs(Number(resultDislikes))) }}</span>
|
|
39
39
|
</button>
|
|
40
40
|
</div>
|
|
41
41
|
</template>
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
@back="activeStep === 'list' ? handleBack() : goToList()"
|
|
11
11
|
>
|
|
12
12
|
<template #title>{{ activeStep !== 'list' && activeCategory ? t(`report_form.${activeCategory.title}`) : t('report') }}</template>
|
|
13
|
-
|
|
14
|
-
<!-- Step 1: Category list -->
|
|
15
|
-
<div v-if="activeStep === 'list'" class="s-report__popup-wrapper">
|
|
13
|
+
<template #fixedContent>
|
|
16
14
|
<SVideoMiniCard
|
|
17
15
|
class="s-report__video-card"
|
|
18
16
|
:card="reportedVideoCard"
|
|
19
17
|
/>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<!-- Step 1: Category list -->
|
|
21
|
+
<div v-if="activeStep === 'list'" class="s-report__popup-wrapper">
|
|
20
22
|
<div class="s-report__categories">
|
|
21
23
|
<button
|
|
22
24
|
v-for="(item, index) in reportFormsScheme"
|
|
@@ -40,10 +42,6 @@
|
|
|
40
42
|
class="s-report__popup-wrapper"
|
|
41
43
|
:class="{'_loading': loading}"
|
|
42
44
|
>
|
|
43
|
-
<SVideoMiniCard
|
|
44
|
-
class="s-report__video-card"
|
|
45
|
-
:card="reportedVideoCard"
|
|
46
|
-
/>
|
|
47
45
|
<div class="s-report__wrapper" v-if="activeCategory">
|
|
48
46
|
<p
|
|
49
47
|
v-if="activeCategory.text"
|
|
@@ -124,8 +122,8 @@
|
|
|
124
122
|
</template>
|
|
125
123
|
|
|
126
124
|
<script setup lang="ts">
|
|
127
|
-
import { reportFormsScheme } from '
|
|
128
|
-
import { EReportFormsSubjects, validateEmail, validatePhone } from '
|
|
125
|
+
import { reportFormsScheme } from 'itube-specs/lib/report-forms-scheme';
|
|
126
|
+
import { EReportFormsSubjects, validateEmail, validatePhone } from 'itube-specs/runtime';
|
|
129
127
|
import type { InputTypes, IReportForm, IReportRequest } from '../../types';
|
|
130
128
|
|
|
131
129
|
const { initRecaptcha, getRecaptchaToken } = useRecaptcha();
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<transition mode="out-in">
|
|
3
3
|
<SPopup
|
|
4
|
-
v-if="
|
|
5
|
-
v-model="
|
|
4
|
+
v-if="model"
|
|
5
|
+
v-model="model"
|
|
6
6
|
sheet
|
|
7
7
|
>
|
|
8
8
|
<template #title>{{ $t('share') }}</template>
|
|
9
|
+
<template v-if="$slots.card" #fixedContent>
|
|
10
|
+
<div class="s-share__card">
|
|
11
|
+
<slot name="card"/>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
9
14
|
<div class="s-share__popup-wrapper">
|
|
10
|
-
<SVideoMiniCard
|
|
11
|
-
v-if="sharedVideoCard && Object.keys(sharedVideoCard).length"
|
|
12
|
-
class="s-share__video-card"
|
|
13
|
-
:card="sharedVideoCard"
|
|
14
|
-
/>
|
|
15
15
|
<div class="s-share__buttons">
|
|
16
16
|
<a
|
|
17
17
|
v-for="(item, index) in buttons"
|
|
@@ -25,37 +25,25 @@
|
|
|
25
25
|
sizes="28px"
|
|
26
26
|
width="28"
|
|
27
27
|
height="28"
|
|
28
|
-
:src="`/
|
|
28
|
+
:src="`/icons/socials/${item.img}`"
|
|
29
29
|
/>
|
|
30
30
|
</a>
|
|
31
31
|
</div>
|
|
32
|
-
<div class="s-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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>
|
|
32
|
+
<div class="s-share__copy">
|
|
33
|
+
<span class="s-share__copy-url">{{ fullUrl }}</span>
|
|
34
|
+
<button
|
|
35
|
+
class="s-share__copy-button"
|
|
36
|
+
:title="$t('copy')"
|
|
37
|
+
@click="copyUrl"
|
|
38
|
+
type="button"
|
|
39
|
+
>
|
|
40
|
+
<SIcon
|
|
41
|
+
class="s-share__copy-icon"
|
|
42
|
+
:class="{'--accent': isCopied}"
|
|
43
|
+
:name="copyIcon"
|
|
44
|
+
size="20"
|
|
45
|
+
/>
|
|
46
|
+
</button>
|
|
59
47
|
</div>
|
|
60
48
|
</div>
|
|
61
49
|
</SPopup>
|
|
@@ -63,15 +51,21 @@
|
|
|
63
51
|
</template>
|
|
64
52
|
|
|
65
53
|
<script setup lang="ts">
|
|
66
|
-
const {
|
|
54
|
+
const model = defineModel<boolean>({ default: false });
|
|
55
|
+
|
|
56
|
+
const props = defineProps<{
|
|
57
|
+
url?: string
|
|
58
|
+
}>();
|
|
67
59
|
|
|
68
60
|
const route = useRoute();
|
|
69
61
|
|
|
70
62
|
const isCopied = ref(false);
|
|
71
63
|
let copyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
72
64
|
|
|
65
|
+
const origin = computed(() => process.client ? window.location.origin : '');
|
|
66
|
+
|
|
73
67
|
const fullUrl = computed(() =>
|
|
74
|
-
|
|
68
|
+
props.url ? origin.value + props.url : origin.value + route.fullPath
|
|
75
69
|
);
|
|
76
70
|
|
|
77
71
|
function copyUrl() {
|
|
@@ -82,7 +76,6 @@ function copyUrl() {
|
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
const copyIcon = computed(() => isCopied.value ? 'check' : 'copy');
|
|
85
|
-
const inputText = computed(() => isCopied.value ? t('link_copied') : t('link'));
|
|
86
79
|
|
|
87
80
|
watch(() => isCopied.value, (value) => {
|
|
88
81
|
if (value) {
|
|
@@ -94,8 +87,6 @@ onBeforeUnmount(() => {
|
|
|
94
87
|
if (copyTimeout) clearTimeout(copyTimeout);
|
|
95
88
|
});
|
|
96
89
|
|
|
97
|
-
const { t } = useI18n()
|
|
98
|
-
|
|
99
90
|
const buttons = computed(() => {
|
|
100
91
|
return [
|
|
101
92
|
{
|
|
@@ -21,73 +21,86 @@
|
|
|
21
21
|
class="s-playlist-add__wrapper"
|
|
22
22
|
:class="{'_loading': loadingUserPlaylists || loadingPostVideo}"
|
|
23
23
|
>
|
|
24
|
+
<SCheckbox
|
|
25
|
+
right
|
|
26
|
+
class="s-playlist-add__item"
|
|
27
|
+
:model-value="selectValue.includes('favorites')"
|
|
28
|
+
@update:model-value="togglePlaylist('favorites')"
|
|
29
|
+
>
|
|
30
|
+
<div class="s-playlist-add__item-wrapper --favorites">
|
|
31
|
+
<SIcon name="heart" size="20" />
|
|
32
|
+
{{ $t('favorites')}}
|
|
33
|
+
</div>
|
|
34
|
+
</SCheckbox>
|
|
24
35
|
<template
|
|
25
36
|
v-if="playlistsItems && playlistsItems.length"
|
|
26
37
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
<template v-for="(item, index) in playlistsItems" :key="`playlist-${index}`">
|
|
39
|
+
<SPlaylistInput
|
|
40
|
+
v-if="editingId === item.value"
|
|
41
|
+
v-model="editName"
|
|
42
|
+
v-model:type="editType"
|
|
43
|
+
:placeholder="$t('playlist_name')"
|
|
44
|
+
edit-playlist
|
|
45
|
+
@close="editingId = null"
|
|
46
|
+
@submit="saveEdit(item.value)"
|
|
47
|
+
@delete="deleteEdit(item.value)"
|
|
48
|
+
/>
|
|
49
|
+
<SCheckbox
|
|
50
|
+
v-else
|
|
51
|
+
class="s-playlist-add__item"
|
|
52
|
+
right
|
|
53
|
+
:model-value="selectValue.includes(item.value)"
|
|
54
|
+
@update:model-value="togglePlaylist(item.value)"
|
|
55
|
+
>
|
|
56
|
+
<div class="s-playlist-add__item-wrapper">
|
|
57
|
+
<SIcon
|
|
58
|
+
class="s-playlist-add__item-icon"
|
|
59
|
+
name="list"
|
|
60
|
+
size="20"
|
|
61
|
+
/>
|
|
62
|
+
<span class="s-playlist-add__item-text">{{ item.title }}</span>
|
|
63
|
+
<SIcon
|
|
64
|
+
class="s-playlist-add__item-icon"
|
|
65
|
+
:name="item.typeIcon"
|
|
66
|
+
size="16"
|
|
67
|
+
/>
|
|
68
|
+
<div class="s-playlist-add__item-icon">
|
|
69
|
+
<SIcon name="video-camera" size="16" />
|
|
70
|
+
{{ item.videosCount }}
|
|
71
|
+
</div>
|
|
56
72
|
</div>
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
<button class="s-playlist-add__item-edit" type="button" @click.prevent="startEdit(item)">
|
|
74
|
+
<SIcon name="pen" size="16"/>
|
|
75
|
+
</button>
|
|
76
|
+
</SCheckbox>
|
|
77
|
+
</template>
|
|
59
78
|
</template>
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
79
|
+
<template v-if="!editingId">
|
|
80
|
+
<button
|
|
81
|
+
v-if="step !== 'new'"
|
|
82
|
+
type="button"
|
|
83
|
+
class="s-playlist-add__item --new"
|
|
84
|
+
@click="step = 'new'"
|
|
85
|
+
>
|
|
86
|
+
<SIcon
|
|
87
|
+
class="s-playlist-add__item-icon --new"
|
|
88
|
+
name="plus"
|
|
89
|
+
size="20"
|
|
90
|
+
/>
|
|
91
|
+
{{ $t('playlist.new_playlist') }}
|
|
92
|
+
</button>
|
|
93
|
+
<SPlaylistInput
|
|
94
|
+
v-else
|
|
75
95
|
v-model="newPlaylistName"
|
|
76
|
-
:
|
|
77
|
-
:icon="newPlaylistName ? 'close' : undefined"
|
|
96
|
+
v-model:type="playlistType"
|
|
78
97
|
:error="nameError"
|
|
79
|
-
|
|
98
|
+
:placeholder="$t('playlist_name')"
|
|
80
99
|
@update:error="nameError = $event"
|
|
100
|
+
@close="clearNewPlaylist"
|
|
101
|
+
@submit="onSaveClick"
|
|
81
102
|
/>
|
|
82
|
-
|
|
83
|
-
size="s"
|
|
84
|
-
theme="primary"
|
|
85
|
-
:disabled="!newPlaylistName.trim()"
|
|
86
|
-
@click="createPlaylist"
|
|
87
|
-
>
|
|
88
|
-
{{ $t('create') }}
|
|
89
|
-
</SButton>
|
|
90
|
-
</div>
|
|
103
|
+
</template>
|
|
91
104
|
</div>
|
|
92
105
|
|
|
93
106
|
<template #footer>
|
|
@@ -105,8 +118,8 @@
|
|
|
105
118
|
size="l"
|
|
106
119
|
:disabled="loadingUserPlaylists || loadingPostVideo"
|
|
107
120
|
theme="primary"
|
|
108
|
-
@click="onSaveClick"
|
|
109
|
-
>{{ $t('add') }}
|
|
121
|
+
@click="editingId ? saveEdit(editingId) : onSaveClick()"
|
|
122
|
+
>{{ editingId ? $t('update') : $t('add') }}
|
|
110
123
|
</SButton>
|
|
111
124
|
</div>
|
|
112
125
|
</template>
|
|
@@ -128,12 +141,14 @@ const props = defineProps<{
|
|
|
128
141
|
}>()
|
|
129
142
|
|
|
130
143
|
const emit = defineEmits<{
|
|
131
|
-
(eventName: 'post-playlist', eventValue: IPlaylistShort): void
|
|
132
144
|
(eventName: 'execute-user-playlists'): void
|
|
133
145
|
(eventName: 'save-video-to-playlist', eventValue: {
|
|
134
146
|
form: IPlaylistVideoFormType
|
|
135
147
|
id: string
|
|
148
|
+
newPlaylist?: IPlaylistShort
|
|
136
149
|
}): void
|
|
150
|
+
(eventName: 'edit-playlist', eventValue: { id: string, playlistName: string, playlistType: EPlaylistType }): void
|
|
151
|
+
(eventName: 'delete-playlist', eventValue: string): void
|
|
137
152
|
}>()
|
|
138
153
|
|
|
139
154
|
const { videoCard, closePlaylistAdd, isPlaylistAdd, selectValue, step } = usePlaylistAdd();
|
|
@@ -147,6 +162,8 @@ const playlistsItems = computed(() => {
|
|
|
147
162
|
title: item.name,
|
|
148
163
|
value: item.id,
|
|
149
164
|
videosCount: item.videosCount,
|
|
165
|
+
typeIcon: item.playlistType === EPlaylistType.Private ? 'lock' : 'user-group',
|
|
166
|
+
playlistType: item.playlistType === EPlaylistType.Private ? 'private' as const : 'public' as const,
|
|
150
167
|
}))
|
|
151
168
|
})
|
|
152
169
|
|
|
@@ -161,45 +178,55 @@ function togglePlaylist(value: string) {
|
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
watch(isPlaylistAdd, (val) => {
|
|
164
|
-
if (val)
|
|
181
|
+
if (val) {
|
|
182
|
+
selectValue.value = ['favorites'];
|
|
183
|
+
emit('execute-user-playlists');
|
|
184
|
+
}
|
|
165
185
|
}, { immediate: true });
|
|
166
186
|
|
|
167
187
|
async function closePopup() {
|
|
168
188
|
step.value = 'add';
|
|
189
|
+
selectValue.value = [];
|
|
190
|
+
newPlaylistName.value = '';
|
|
191
|
+
playlistType.value = 'public';
|
|
192
|
+
editingId.value = null;
|
|
169
193
|
isPlaylistAdd.value = false;
|
|
170
194
|
closePlaylistAdd();
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
const newPlaylistName = ref('');
|
|
174
|
-
const
|
|
198
|
+
const playlistType = ref<'public' | 'private'>('public');
|
|
175
199
|
const nameError = ref(false);
|
|
176
200
|
|
|
177
201
|
function clearNewPlaylist() {
|
|
178
202
|
newPlaylistName.value = '';
|
|
203
|
+
playlistType.value = 'public';
|
|
204
|
+
step.value = 'add';
|
|
179
205
|
}
|
|
180
206
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const nameExists = props.userPlaylists?.items?.some(
|
|
186
|
-
(item: IPlaylistCard) => item.name.toLowerCase() === name.toLowerCase()
|
|
187
|
-
);
|
|
207
|
+
const editingId = ref<string | null>(null);
|
|
208
|
+
const editName = ref('');
|
|
209
|
+
const editType = ref<'public' | 'private'>('public');
|
|
188
210
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
function startEdit(item: { value: string, title: string, playlistType: 'public' | 'private' }) {
|
|
212
|
+
clearNewPlaylist();
|
|
213
|
+
editingId.value = item.value;
|
|
214
|
+
editName.value = item.title;
|
|
215
|
+
editType.value = item.playlistType;
|
|
216
|
+
}
|
|
195
217
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
function saveEdit(id: string) {
|
|
219
|
+
emit('edit-playlist', {
|
|
220
|
+
id,
|
|
221
|
+
playlistName: editName.value,
|
|
222
|
+
playlistType: editType.value === 'private' ? EPlaylistType.Private : EPlaylistType.Public,
|
|
199
223
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
editingId.value = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function deleteEdit(id: string) {
|
|
228
|
+
editingId.value = null;
|
|
229
|
+
emit('delete-playlist', id);
|
|
203
230
|
}
|
|
204
231
|
|
|
205
232
|
const postVideoForm = computed(() => ({
|
|
@@ -215,10 +242,34 @@ const postVideoForm = computed(() => ({
|
|
|
215
242
|
const { setErrorState, snackbarText, snackbarTheme } = useSnackbar();
|
|
216
243
|
|
|
217
244
|
async function onSaveClick() {
|
|
245
|
+
let newPlaylist: IPlaylistShort | undefined;
|
|
246
|
+
|
|
247
|
+
if (step.value === 'new') {
|
|
248
|
+
const name = newPlaylistName.value.trim();
|
|
249
|
+
if (!name) return;
|
|
250
|
+
|
|
251
|
+
const nameExists = props.userPlaylists?.items?.some(
|
|
252
|
+
(item: IPlaylistCard) => item.name.toLowerCase() === name.toLowerCase()
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (nameExists) {
|
|
256
|
+
nameError.value = true;
|
|
257
|
+
snackbarTheme.value = 'error';
|
|
258
|
+
snackbarText.value = 'playlist_name_exists';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
newPlaylist = {
|
|
263
|
+
name,
|
|
264
|
+
type: playlistType.value === 'private' ? EPlaylistType.Private : EPlaylistType.Public,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
218
268
|
try {
|
|
219
269
|
emit('save-video-to-playlist', {
|
|
220
270
|
form: postVideoForm.value as IPlaylistVideoFormType,
|
|
221
|
-
id: String(selectValue.value)
|
|
271
|
+
id: String(selectValue.value),
|
|
272
|
+
newPlaylist,
|
|
222
273
|
})
|
|
223
274
|
closePopup();
|
|
224
275
|
snackbarTheme.value = 'success';
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
sheet
|
|
7
7
|
transparent-backdrop
|
|
8
8
|
>
|
|
9
|
-
<template #title>{{ $t('
|
|
9
|
+
<template #title>{{ $t('remove_video') }}</template>
|
|
10
10
|
|
|
11
11
|
<div
|
|
12
12
|
class="s-playlist-delete-video__wrapper"
|
|
13
13
|
:class="{'_loading': loadingAll}"
|
|
14
14
|
>
|
|
15
15
|
<SVideoMiniCard v-if="deletedVideo" :card="deletedVideo"/>
|
|
16
|
-
<p class="s-playlist-delete-video__text">{{ $t('
|
|
16
|
+
<p class="s-playlist-delete-video__text">{{ $t('confirm_remove_video') }}</p>
|
|
17
17
|
</div>
|
|
18
18
|
<template #footer>
|
|
19
19
|
<div class="s-playlist-delete-video__buttons">
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
size="l"
|
|
32
32
|
theme="primary"
|
|
33
33
|
@click="onBinClick"
|
|
34
|
-
>{{ $t('
|
|
34
|
+
>{{ $t('remove') }}
|
|
35
35
|
</SButton>
|
|
36
36
|
</div>
|
|
37
37
|
</template>
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
</template>
|
|
42
42
|
|
|
43
43
|
<script setup lang="ts">
|
|
44
|
-
import { EPlaylistStep } from '
|
|
44
|
+
import { EPlaylistStep } from 'itube-specs/runtime';
|
|
45
45
|
|
|
46
46
|
const { isPlaylistEditOpen, closePlaylistEdit, currentStep, selectedPlaylist, deletedVideo } = usePlaylistEdit();
|
|
47
47
|
const { snackbarText, snackbarTheme } = useSnackbar();
|
|
@@ -69,7 +69,7 @@ async function onBinClick() {
|
|
|
69
69
|
deletedVideoId: deletedVideo.value?.id || ''
|
|
70
70
|
})
|
|
71
71
|
snackbarTheme.value = 'success';
|
|
72
|
-
snackbarText.value = '
|
|
72
|
+
snackbarText.value = 'video_removed';
|
|
73
73
|
closePlaylistEdit();
|
|
74
74
|
} catch {
|
|
75
75
|
console.log('error delete')
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="s-playlist-input"
|
|
4
|
+
@keydown.enter="$emit('submit')"
|
|
5
|
+
>
|
|
6
|
+
<SInput
|
|
7
|
+
v-model="name"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
:error="error"
|
|
10
|
+
:icon="name.length > 0 ? 'close' : ''"
|
|
11
|
+
@icon-click="name = ''"
|
|
12
|
+
@update:error="$emit('update:error', $event)"
|
|
13
|
+
/>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
class="s-playlist-input__back"
|
|
17
|
+
@click="$emit('close')"
|
|
18
|
+
>
|
|
19
|
+
<SIcon name="close" size="20"/>
|
|
20
|
+
</button>
|
|
21
|
+
<div class="s-playlist-input__visibility">
|
|
22
|
+
<SToggle
|
|
23
|
+
:model-value="currentType === 'public'"
|
|
24
|
+
@update:model-value="currentType = $event ? 'public' : 'private'"
|
|
25
|
+
>
|
|
26
|
+
<div class="s-playlist-input__toggle-wrapper">
|
|
27
|
+
<SIcon class="s-playlist-input__toggle-icon" :name="PLAYLIST_TYPE_ICON[EPlaylistType.Public]" size="14"/>
|
|
28
|
+
{{ $t('public') }}
|
|
29
|
+
</div>
|
|
30
|
+
</SToggle>
|
|
31
|
+
<FTooltip class="s-playlist-input__tooltip" right>
|
|
32
|
+
<template #trigger>
|
|
33
|
+
<span class="s-playlist-input__hint">?</span>
|
|
34
|
+
</template>
|
|
35
|
+
<p class="s-playlist-input__tooltip-text"><span>{{ $t('public') }}: </span>{{ $t('public_hint') }}</p>
|
|
36
|
+
<p class="s-playlist-input__tooltip-text"><span>{{ $t('private') }}: </span>{{ $t('private_hint') }}</p>
|
|
37
|
+
</FTooltip>
|
|
38
|
+
<button
|
|
39
|
+
v-if="editPlaylist"
|
|
40
|
+
type="button"
|
|
41
|
+
class="s-playlist-input__delete"
|
|
42
|
+
@click="$emit('delete')"
|
|
43
|
+
>
|
|
44
|
+
{{ $t('delete') }}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import { EPlaylistType } from 'itube-specs/runtime';
|
|
52
|
+
|
|
53
|
+
const PLAYLIST_TYPE_ICON: Record<EPlaylistType, string> = {
|
|
54
|
+
[EPlaylistType.Public]: 'user-group',
|
|
55
|
+
[EPlaylistType.Private]: 'lock',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const props = withDefaults(defineProps<{
|
|
59
|
+
modelValue: string
|
|
60
|
+
type: 'public' | 'private'
|
|
61
|
+
error?: boolean
|
|
62
|
+
placeholder?: string
|
|
63
|
+
editPlaylist?: boolean
|
|
64
|
+
}>(), {
|
|
65
|
+
error: false,
|
|
66
|
+
placeholder: '',
|
|
67
|
+
editPlaylist: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits<{
|
|
71
|
+
(eventName: 'update:modelValue', value: string): void
|
|
72
|
+
(eventName: 'update:type', value: 'public' | 'private'): void
|
|
73
|
+
(eventName: 'update:error', value: boolean): void
|
|
74
|
+
(eventName: 'close'): void
|
|
75
|
+
(eventName: 'submit'): void
|
|
76
|
+
(eventName: 'delete'): void
|
|
77
|
+
}>();
|
|
78
|
+
|
|
79
|
+
const name = computed({
|
|
80
|
+
get: () => props.modelValue,
|
|
81
|
+
set: (val: string) => emit('update:modelValue', val),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const currentType = computed({
|
|
85
|
+
get: () => props.type,
|
|
86
|
+
set: (val: 'public' | 'private') => emit('update:type', val),
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="s-avatar">
|
|
3
|
+
<img
|
|
4
|
+
v-if="src || data.avatar"
|
|
5
|
+
:src="avatar"
|
|
6
|
+
:width="size"
|
|
7
|
+
:height="size"
|
|
8
|
+
:alt="$t(alt)"
|
|
9
|
+
>
|
|
10
|
+
<span
|
|
11
|
+
v-else
|
|
12
|
+
class="s-avatar__placeholder"
|
|
13
|
+
:style="{ width: size + 'px', height: size + 'px' }"
|
|
14
|
+
>{{ avatar }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import type { IProfileData } from 'itube-specs';
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<{
|
|
22
|
+
size?: number
|
|
23
|
+
alt?: string
|
|
24
|
+
src?: string
|
|
25
|
+
}>(), {
|
|
26
|
+
size: 36,
|
|
27
|
+
alt: 'avatar'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const data = computed(() => useState<IProfileData>('profileData').value);
|
|
31
|
+
|
|
32
|
+
const avatar = computed(() => props.src || (data.value && (data.value.avatar || data.value.username.charAt(0))));
|
|
33
|
+
</script>
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
</template>
|
|
20
20
|
|
|
21
21
|
<script setup lang="ts">
|
|
22
|
+
import type { ButtonThemes } from '../../types';
|
|
23
|
+
|
|
22
24
|
withDefaults(
|
|
23
25
|
defineProps<{
|
|
24
26
|
type?: 'button' | 'submit' | 'reset',
|
|
25
|
-
theme?:
|
|
27
|
+
theme?: ButtonThemes
|
|
26
28
|
size?: 'm' | 's' | 'l',
|
|
27
29
|
icon?: boolean,
|
|
28
30
|
wide?: boolean,
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
<script setup lang="ts">
|
|
50
50
|
// На компоненте обязательно вызывать пропсы слота и вешать слот пропс (className) на непосредственных детей
|
|
51
|
-
import { isMobileWidth } from '
|
|
51
|
+
import { isMobileWidth } from 'itube-specs/runtime';
|
|
52
52
|
import type { CssBreakpoints } from '../../types';
|
|
53
53
|
import { onClickOutside } from '@vueuse/core';
|
|
54
54
|
|
|
@@ -93,11 +93,10 @@ function openDropdown() {
|
|
|
93
93
|
open.value = !open.value
|
|
94
94
|
if (open.value) nextTick(() => checkPosition())
|
|
95
95
|
|
|
96
|
-
if (isMobile.value) {
|
|
96
|
+
if (isMobile.value && open.value) {
|
|
97
97
|
nextTick(() => {
|
|
98
98
|
if (menuRef.value?.showModal) {
|
|
99
99
|
menuRef.value.showModal();
|
|
100
|
-
scrollLock();
|
|
101
100
|
}
|
|
102
101
|
})
|
|
103
102
|
}
|
|
@@ -107,13 +106,27 @@ function close() {
|
|
|
107
106
|
open.value = false;
|
|
108
107
|
|
|
109
108
|
if (isMobile.value) {
|
|
110
|
-
scrollUnlock()
|
|
111
109
|
nextTick(() => {
|
|
112
110
|
menuRef.value?.close?.()
|
|
113
111
|
})
|
|
114
112
|
}
|
|
115
113
|
}
|
|
116
114
|
|
|
115
|
+
watch(open, (val) => {
|
|
116
|
+
if (!isMobile.value) return;
|
|
117
|
+
if (val) {
|
|
118
|
+
scrollLock();
|
|
119
|
+
} else {
|
|
120
|
+
scrollUnlock();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
onBeforeUnmount(() => {
|
|
125
|
+
if (open.value && isMobile.value) {
|
|
126
|
+
scrollUnlock();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
117
130
|
let hoverTimeout: ReturnType<typeof setTimeout> | null = null
|
|
118
131
|
|
|
119
132
|
function mouseHandler(event: boolean) {
|
package/components/ui/s-link.vue
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
</template>
|
|
18
18
|
|
|
19
19
|
<script setup lang="ts">
|
|
20
|
+
import type { ButtonThemes } from '../../types';
|
|
21
|
+
|
|
20
22
|
withDefaults(
|
|
21
23
|
defineProps<{
|
|
22
|
-
theme?:
|
|
24
|
+
theme?: ButtonThemes,
|
|
23
25
|
size?: 'm' | 's',
|
|
24
26
|
wide?: boolean,
|
|
25
27
|
active?: boolean,
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
aria-label="select"
|
|
5
5
|
:class="[
|
|
6
6
|
{
|
|
7
|
-
'--wide': wide,
|
|
8
7
|
'--active': active
|
|
9
8
|
},
|
|
10
9
|
`--${size}`,
|
|
@@ -14,11 +13,11 @@
|
|
|
14
13
|
<SIcon
|
|
15
14
|
v-if="labelIcon"
|
|
16
15
|
class="s-select__label-icon"
|
|
17
|
-
:name="labelIcon"
|
|
16
|
+
:name="labelIcon.icon"
|
|
18
17
|
size="16"
|
|
18
|
+
:prefix="labelIcon.prefix"
|
|
19
19
|
/>
|
|
20
20
|
{{ label }}
|
|
21
|
-
<!-- <SCount class="s-select__count" v-if="count">{{ count }}</SCount>-->
|
|
22
21
|
</span>
|
|
23
22
|
<span class="s-select__wrapper">
|
|
24
23
|
<SIcon
|
|
@@ -58,22 +57,26 @@
|
|
|
58
57
|
|
|
59
58
|
<script setup lang="ts">
|
|
60
59
|
import type { ISelectItem } from '../../types';
|
|
61
|
-
import
|
|
62
|
-
import { convertString } from '~/runtime';
|
|
60
|
+
import { convertString } from 'itube-specs/runtime';
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
export interface ISelectLabelIcon {
|
|
63
|
+
icon: string
|
|
64
|
+
prefix?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ISelectProps {
|
|
65
68
|
name: string
|
|
66
|
-
icon?: string
|
|
67
|
-
labelIcon?: string
|
|
68
|
-
placeholder?: string
|
|
69
69
|
modelValue: string | undefined
|
|
70
|
+
items: ISelectItem[]
|
|
71
|
+
placeholder?: string
|
|
70
72
|
label?: string
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
icon?: string
|
|
74
|
+
labelIcon?: ISelectLabelIcon
|
|
73
75
|
size?: 'm' | 's'
|
|
74
|
-
// count?: string | number
|
|
75
76
|
active?: boolean
|
|
76
|
-
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const props = withDefaults(defineProps<ISelectProps>(), {
|
|
77
80
|
size: 'm'
|
|
78
81
|
})
|
|
79
82
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
aria-label="Toggle"
|
|
5
5
|
:class="[
|
|
6
6
|
{'s-toggle--checked': modelValue},
|
|
7
|
-
{'s-toggle--disabled': disabled}
|
|
7
|
+
{'s-toggle--disabled': disabled},
|
|
8
|
+
{'--left': left}
|
|
8
9
|
]"
|
|
9
10
|
>
|
|
10
11
|
<input
|
|
@@ -16,7 +17,6 @@
|
|
|
16
17
|
>
|
|
17
18
|
<span v-if="$slots.default" class="s-toggle__text">
|
|
18
19
|
<slot/>
|
|
19
|
-
{{ modelValue}}
|
|
20
20
|
</span>
|
|
21
21
|
</label>
|
|
22
22
|
</template>
|
|
@@ -25,5 +25,6 @@
|
|
|
25
25
|
defineProps<{
|
|
26
26
|
modelValue: boolean
|
|
27
27
|
disabled?: boolean
|
|
28
|
+
left?: boolean
|
|
28
29
|
}>()
|
|
29
30
|
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="tooltipRef"
|
|
4
|
+
class="s-tooltip"
|
|
5
|
+
@mouseenter="onEnter"
|
|
6
|
+
@mouseleave="onLeave"
|
|
7
|
+
>
|
|
8
|
+
<div class="s-tooltip__trigger" @click="onClick">
|
|
9
|
+
<slot name="trigger"/>
|
|
10
|
+
</div>
|
|
11
|
+
<transition name="fade">
|
|
12
|
+
<div v-if="isOpen" class="s-tooltip__content" :class="{'--right': right}">
|
|
13
|
+
<slot/>
|
|
14
|
+
</div>
|
|
15
|
+
</transition>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { isMobileWidth } from 'itube-specs/runtime';
|
|
21
|
+
import { onClickOutside } from '@vueuse/core';
|
|
22
|
+
import type { CssBreakpoints } from '../../types';
|
|
23
|
+
|
|
24
|
+
defineProps<{
|
|
25
|
+
right?: boolean
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const tooltipRef = ref<HTMLElement>();
|
|
29
|
+
const isOpen = ref(false);
|
|
30
|
+
|
|
31
|
+
const breakpoints = useAppConfig().cssBreakpoints as Record<CssBreakpoints, number>;
|
|
32
|
+
const isMobile = isMobileWidth(breakpoints);
|
|
33
|
+
|
|
34
|
+
onClickOutside(tooltipRef, () => {
|
|
35
|
+
if (isMobile.value) {
|
|
36
|
+
isOpen.value = false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function onEnter() {
|
|
41
|
+
if (!isMobile.value) {
|
|
42
|
+
isOpen.value = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onLeave() {
|
|
47
|
+
if (!isMobile.value) {
|
|
48
|
+
isOpen.value = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onClick() {
|
|
53
|
+
if (isMobile.value) {
|
|
54
|
+
isOpen.value = !isOpen.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IFilterScheme, IGroupCategories } from '../types';
|
|
2
|
-
import { convertString } from '
|
|
2
|
+
import { convertString } from 'itube-specs/runtime';
|
|
3
3
|
|
|
4
4
|
export const useFilterScheme = (t, groupsCategories: IGroupCategories[] | null) => {
|
|
5
5
|
const getFilterSchemeCategories = (): IFilterScheme[] => {
|
|
@@ -7,6 +7,7 @@ export const useFilterScheme = (t, groupsCategories: IGroupCategories[] | null)
|
|
|
7
7
|
label: item.title,
|
|
8
8
|
name: item.name,
|
|
9
9
|
title: item.title,
|
|
10
|
+
icon: item.icon,
|
|
10
11
|
placeholder: t('choose_category'),
|
|
11
12
|
items: item.categories?.map((subItem) => ({
|
|
12
13
|
title: convertString().toCapitalize(subItem.title),
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
function hasScrollableParent(target: HTMLElement): boolean {
|
|
2
|
+
let el: HTMLElement | null = target;
|
|
3
|
+
while (el && el !== document.body) {
|
|
4
|
+
const { overflowY } = window.getComputedStyle(el);
|
|
5
|
+
if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
el = el.parentElement;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function preventTouchScroll(e: TouchEvent) {
|
|
14
|
+
if (!hasScrollableParent(e.target as HTMLElement)) {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
export function useGlobalScrollLock() {
|
|
4
20
|
const lockCount = useState('global-scroll-lock-count', () => 0);
|
|
5
|
-
let isLockedRef: ReturnType<typeof useScrollLock> | null = null;
|
|
6
|
-
|
|
7
|
-
function getLockedRef() {
|
|
8
|
-
if (!isLockedRef && import.meta.client) {
|
|
9
|
-
isLockedRef = useScrollLock(document.body);
|
|
10
|
-
}
|
|
11
|
-
return isLockedRef;
|
|
12
|
-
}
|
|
13
21
|
|
|
14
22
|
function scrollLock() {
|
|
15
23
|
lockCount.value++;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
if (lockCount.value === 1 && import.meta.client) {
|
|
25
|
+
document.body.style.overflow = 'hidden';
|
|
26
|
+
document.addEventListener('touchmove', preventTouchScroll, { passive: false });
|
|
19
27
|
}
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -23,12 +31,12 @@ export function useGlobalScrollLock() {
|
|
|
23
31
|
lockCount.value--;
|
|
24
32
|
if (lockCount.value <= 0) {
|
|
25
33
|
lockCount.value = 0;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
if (import.meta.client) {
|
|
35
|
+
document.body.style.overflow = '';
|
|
36
|
+
document.removeEventListener('touchmove', preventTouchScroll);
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
return { scrollLock, scrollUnlock }
|
|
41
|
+
return { scrollLock, scrollUnlock };
|
|
34
42
|
}
|
package/package.json
CHANGED
package/types/button-themes.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type ButtonThemes = 'primary' | 'secondary' | '
|
|
1
|
+
export type ButtonThemes = 'primary' | 'secondary' | 'warning' | 'muted' | undefined
|