itube-specs 0.0.910 → 0.0.912
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/scss/themes/dark/_theme.scss +22 -18
- package/assets/scss/themes/light/_theme.scss +21 -17
- package/components/auth/register.vue +0 -4
- package/components/library/feed.vue +54 -24
- package/components/library/item.vue +112 -60
- package/components/playlist/add.vue +123 -113
- package/components/playlist/edit.vue +133 -199
- package/components/playlist/input.vue +23 -133
- package/components/playlist/main.vue +21 -40
- package/components/playlist/visibility.vue +149 -0
- package/components/ui/confirm.vue +17 -5
- package/components/ui/popup.vue +4 -7
- package/components/ui/tooltip.vue +35 -14
- package/composables/use-playlist-edit.ts +13 -3
- package/composables/use-snackbar.ts +15 -1
- package/composables/use-user.ts +0 -4
- package/package.json +1 -1
- package/pages/library/categories.vue +1 -0
- package/pages/library/channels.vue +2 -0
- package/pages/library/index.vue +22 -13
- package/pages/library/models.vue +2 -0
- package/pages/playlists/index.vue +3 -72
- package/runtime/constants/playlist-type.ts +5 -0
- package/stubs/library.ts +0 -1
- package/types/index.d.ts +1 -0
- package/types/library-feed.d.ts +2 -0
- package/types/library-item.d.ts +6 -0
- package/components/playlist/private-toggle.vue +0 -20
- package/composables/use-favorites.ts +0 -20
- package/pages/favorites.vue +0 -31
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
:back="!!onBack"
|
|
8
8
|
class="playlist-add"
|
|
9
9
|
sheet
|
|
10
|
+
transparent-backdrop
|
|
10
11
|
@back="handleBack"
|
|
11
12
|
@close="closePopup"
|
|
12
13
|
>
|
|
@@ -23,37 +24,12 @@
|
|
|
23
24
|
:class="{'_loading': loadingUserPlaylists || loadingPostVideo}"
|
|
24
25
|
class="playlist-add__wrapper"
|
|
25
26
|
>
|
|
26
|
-
<UiCheckbox
|
|
27
|
-
right
|
|
28
|
-
:model-value="selectValue.includes('favorites')"
|
|
29
|
-
class="playlist-add__item"
|
|
30
|
-
@update:model-value="togglePlaylist('favorites')"
|
|
31
|
-
>
|
|
32
|
-
<div class="playlist-add__item-wrapper --favorites">
|
|
33
|
-
<UiIcon
|
|
34
|
-
name="heart"
|
|
35
|
-
size="20"
|
|
36
|
-
/>
|
|
37
|
-
{{ $t('favorites')}}
|
|
38
|
-
</div>
|
|
39
|
-
</UiCheckbox>
|
|
40
27
|
<template v-if="playlistsItems && playlistsItems.length">
|
|
41
28
|
<template
|
|
42
29
|
v-for="(item, index) in playlistsItems"
|
|
43
30
|
:key="`playlist-${index}`"
|
|
44
31
|
>
|
|
45
|
-
<PlaylistInput
|
|
46
|
-
v-if="editingId === item.value"
|
|
47
|
-
v-model="editName"
|
|
48
|
-
v-model:type="editType"
|
|
49
|
-
:placeholder="$t('playlist_name')"
|
|
50
|
-
edit-playlist
|
|
51
|
-
@close="editingId = null"
|
|
52
|
-
@submit="saveEdit(item.value)"
|
|
53
|
-
@delete="deleteEdit(item.value)"
|
|
54
|
-
/>
|
|
55
32
|
<UiCheckbox
|
|
56
|
-
v-else
|
|
57
33
|
:model-value="selectValue.includes(item.value)"
|
|
58
34
|
class="playlist-add__item"
|
|
59
35
|
right
|
|
@@ -62,27 +38,27 @@
|
|
|
62
38
|
<div class="playlist-add__item-wrapper">
|
|
63
39
|
<UiIcon
|
|
64
40
|
class="playlist-add__item-icon"
|
|
65
|
-
name="
|
|
41
|
+
name="playlist"
|
|
66
42
|
size="20"
|
|
67
43
|
/>
|
|
68
44
|
<span class="playlist-add__item-text">{{ item.title }}</span>
|
|
69
45
|
<UiIcon
|
|
70
46
|
:name="item.typeIcon"
|
|
71
47
|
class="playlist-add__item-icon"
|
|
72
|
-
size="
|
|
48
|
+
size="14"
|
|
73
49
|
/>
|
|
74
|
-
<
|
|
50
|
+
<span class="playlist-add__item-icon">
|
|
75
51
|
<UiIcon
|
|
76
52
|
name="video-camera"
|
|
77
|
-
size="
|
|
53
|
+
size="14"
|
|
78
54
|
/>
|
|
79
55
|
{{ item.videosCount }}
|
|
80
|
-
</
|
|
56
|
+
</span>
|
|
81
57
|
</div>
|
|
82
58
|
<button
|
|
83
59
|
class="playlist-add__item-edit"
|
|
84
60
|
type="button"
|
|
85
|
-
@click.prevent="
|
|
61
|
+
@click.prevent="openEdit(item.card)"
|
|
86
62
|
>
|
|
87
63
|
<UiIcon
|
|
88
64
|
name="pen"
|
|
@@ -92,31 +68,29 @@
|
|
|
92
68
|
</UiCheckbox>
|
|
93
69
|
</template>
|
|
94
70
|
</template>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
size="20"
|
|
106
|
-
/>
|
|
107
|
-
{{ $t('playlist.new_playlist') }}
|
|
108
|
-
</button>
|
|
109
|
-
<PlaylistInput
|
|
110
|
-
v-else
|
|
111
|
-
v-model="newPlaylistName"
|
|
112
|
-
v-model:type="playlistType"
|
|
113
|
-
:error="nameError"
|
|
114
|
-
:placeholder="$t('playlist_name')"
|
|
115
|
-
@update:error="nameError = $event"
|
|
116
|
-
@close="clearNewPlaylist"
|
|
117
|
-
@submit="onSaveClick"
|
|
71
|
+
<button
|
|
72
|
+
v-if="step !== 'new'"
|
|
73
|
+
type="button"
|
|
74
|
+
class="playlist-add__item --new"
|
|
75
|
+
@click="step = 'new'"
|
|
76
|
+
>
|
|
77
|
+
<UiIcon
|
|
78
|
+
class="playlist-add__item-icon --new"
|
|
79
|
+
name="plus"
|
|
80
|
+
size="20"
|
|
118
81
|
/>
|
|
119
|
-
|
|
82
|
+
{{ $t('playlist.new_playlist') }}
|
|
83
|
+
</button>
|
|
84
|
+
<PlaylistInput
|
|
85
|
+
v-else
|
|
86
|
+
v-model="newPlaylistName"
|
|
87
|
+
v-model:type="playlistType"
|
|
88
|
+
:error="nameError"
|
|
89
|
+
:placeholder="$t('playlist_name')"
|
|
90
|
+
@update:error="nameError = $event"
|
|
91
|
+
@close="clearNewPlaylist"
|
|
92
|
+
@submit="onSaveClick"
|
|
93
|
+
/>
|
|
120
94
|
</div>
|
|
121
95
|
|
|
122
96
|
<template #footer>
|
|
@@ -131,11 +105,11 @@
|
|
|
131
105
|
</UiBtn>
|
|
132
106
|
<UiBtn
|
|
133
107
|
wide
|
|
134
|
-
:disabled="
|
|
108
|
+
:disabled="submitDisabled"
|
|
135
109
|
size="l"
|
|
136
110
|
theme="primary"
|
|
137
|
-
@click="
|
|
138
|
-
>{{
|
|
111
|
+
@click="onSaveClick"
|
|
112
|
+
>{{ $t('add') }}
|
|
139
113
|
</UiBtn>
|
|
140
114
|
</div>
|
|
141
115
|
</template>
|
|
@@ -147,7 +121,7 @@
|
|
|
147
121
|
|
|
148
122
|
<script setup lang="ts">
|
|
149
123
|
import type { IPlaylistVideoFormType, IPlaylistShort, IPlaylistCard, PaginatedResponse } from '../../types';
|
|
150
|
-
import { PlaylistType } from '../../runtime';
|
|
124
|
+
import { PLAYLIST_TYPE_ICON, PlaylistStep, PlaylistType } from '../../runtime';
|
|
151
125
|
|
|
152
126
|
const props = defineProps<{
|
|
153
127
|
loadingPostPlaylist: boolean
|
|
@@ -163,11 +137,22 @@ const emit = defineEmits<{
|
|
|
163
137
|
id: string
|
|
164
138
|
newPlaylist?: IPlaylistShort
|
|
165
139
|
}): void
|
|
166
|
-
(eventName: 'edit-playlist', eventValue: { id: string, playlistName: string, playlistType: PlaylistType }): void
|
|
167
|
-
(eventName: 'delete-playlist', eventValue: string): void
|
|
168
140
|
}>();
|
|
169
141
|
|
|
170
142
|
const { videoCard, closePlaylistAdd, isPlaylistAdd, selectValue, step, onBack } = usePlaylistAdd();
|
|
143
|
+
const { openPlaylistEdit, onBack: onEditBack } = usePlaylistEdit();
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Редактирование открывается вместо добавления, не поверх. Карточку видео не сбрасываем —
|
|
147
|
+
* из редактирования по «назад» возвращаемся сюда же.
|
|
148
|
+
*/
|
|
149
|
+
function openEdit(card: IPlaylistCard) {
|
|
150
|
+
onEditBack.value = () => {
|
|
151
|
+
isPlaylistAdd.value = true;
|
|
152
|
+
};
|
|
153
|
+
isPlaylistAdd.value = false;
|
|
154
|
+
openPlaylistEdit(PlaylistStep.Edit, false, card);
|
|
155
|
+
}
|
|
171
156
|
|
|
172
157
|
function handleBack() {
|
|
173
158
|
const callback = onBack.value;
|
|
@@ -175,17 +160,14 @@ function handleBack() {
|
|
|
175
160
|
callback?.();
|
|
176
161
|
}
|
|
177
162
|
|
|
178
|
-
const { FAVORITES_KEY } = useFavorites();
|
|
179
|
-
|
|
180
163
|
const playlistsItems = computed(() => {
|
|
181
164
|
return props.userPlaylists?.items
|
|
182
|
-
?.
|
|
183
|
-
.map((item: IPlaylistCard) => ({
|
|
165
|
+
?.map((item: IPlaylistCard) => ({
|
|
184
166
|
title: item.name,
|
|
185
167
|
value: item.id,
|
|
186
168
|
videosCount: item.videosCount,
|
|
187
|
-
typeIcon: item.playlistType
|
|
188
|
-
|
|
169
|
+
typeIcon: PLAYLIST_TYPE_ICON[item.playlistType] ?? PLAYLIST_TYPE_ICON[PlaylistType.Public],
|
|
170
|
+
card: item,
|
|
189
171
|
}));
|
|
190
172
|
});
|
|
191
173
|
|
|
@@ -201,7 +183,7 @@ function togglePlaylist(value: string) {
|
|
|
201
183
|
|
|
202
184
|
watch(isPlaylistAdd, (val) => {
|
|
203
185
|
if (val) {
|
|
204
|
-
selectValue.value = [
|
|
186
|
+
selectValue.value = [];
|
|
205
187
|
emit('execute-user-playlists');
|
|
206
188
|
}
|
|
207
189
|
}, { immediate: true });
|
|
@@ -210,46 +192,26 @@ async function closePopup() {
|
|
|
210
192
|
step.value = 'add';
|
|
211
193
|
selectValue.value = [];
|
|
212
194
|
newPlaylistName.value = '';
|
|
213
|
-
playlistType.value =
|
|
214
|
-
editingId.value = null;
|
|
195
|
+
playlistType.value = PlaylistType.Public;
|
|
215
196
|
isPlaylistAdd.value = false;
|
|
216
197
|
closePlaylistAdd();
|
|
217
198
|
}
|
|
218
199
|
|
|
219
200
|
const newPlaylistName = ref('');
|
|
220
|
-
const playlistType = ref<
|
|
201
|
+
const playlistType = ref<PlaylistType>(PlaylistType.Public);
|
|
221
202
|
const nameError = ref(false);
|
|
222
203
|
|
|
223
204
|
function clearNewPlaylist() {
|
|
224
205
|
newPlaylistName.value = '';
|
|
225
|
-
playlistType.value =
|
|
206
|
+
playlistType.value = PlaylistType.Public;
|
|
226
207
|
step.value = 'add';
|
|
227
208
|
}
|
|
228
209
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
clearNewPlaylist();
|
|
235
|
-
editingId.value = item.value;
|
|
236
|
-
editName.value = item.title;
|
|
237
|
-
editType.value = item.playlistType;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function saveEdit(id: string) {
|
|
241
|
-
emit('edit-playlist', {
|
|
242
|
-
id,
|
|
243
|
-
playlistName: editName.value,
|
|
244
|
-
playlistType: editType.value === 'private' ? PlaylistType.Private : PlaylistType.Public,
|
|
245
|
-
});
|
|
246
|
-
editingId.value = null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function deleteEdit(id: string) {
|
|
250
|
-
editingId.value = null;
|
|
251
|
-
emit('delete-playlist', id);
|
|
252
|
-
}
|
|
210
|
+
const submitDisabled = computed(() => {
|
|
211
|
+
if (props.loadingUserPlaylists || props.loadingPostVideo) return true;
|
|
212
|
+
if (step.value === 'new') return !newPlaylistName.value.trim();
|
|
213
|
+
return !selectValue.value.length;
|
|
214
|
+
});
|
|
253
215
|
|
|
254
216
|
const postVideoForm = computed(() => ({
|
|
255
217
|
playlistId: selectValue.value.length ? selectValue.value : undefined,
|
|
@@ -262,6 +224,35 @@ const postVideoForm = computed(() => ({
|
|
|
262
224
|
}));
|
|
263
225
|
|
|
264
226
|
const { setErrorState, show, showError } = useSnackbar();
|
|
227
|
+
const { generateLink } = useGenerateLink();
|
|
228
|
+
const { t } = useI18n();
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Куда добавили: при одном плейлисте — его имя и ссылка на него же, при нескольких —
|
|
232
|
+
* количество и ссылка на список. У нового плейлиста id появляется только после ответа
|
|
233
|
+
* бэка, поэтому для него тоже ссылка на список.
|
|
234
|
+
*/
|
|
235
|
+
function getTarget(newPlaylistName?: string) {
|
|
236
|
+
const selected = props.userPlaylists?.items
|
|
237
|
+
?.filter((item: IPlaylistCard) => selectValue.value.includes(item.id)) ?? [];
|
|
238
|
+
const total = selected.length + (newPlaylistName ? 1 : 0);
|
|
239
|
+
const single = total === 1 ? selected[0] : undefined;
|
|
240
|
+
|
|
241
|
+
if (single) {
|
|
242
|
+
// Пустому плейлисту первым видео становится только что добавленное.
|
|
243
|
+
const firstVideoId = single.firstVideoId ?? videoCard.value?.id;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
label: single.name,
|
|
247
|
+
link: generateLink(`/playlists/${single.id}/${firstVideoId}`),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
label: total === 1 ? newPlaylistName! : t('plural.playlists', total),
|
|
253
|
+
link: generateLink('/playlists'),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
265
256
|
|
|
266
257
|
async function onSaveClick() {
|
|
267
258
|
let newPlaylist: IPlaylistShort | undefined;
|
|
@@ -282,10 +273,12 @@ async function onSaveClick() {
|
|
|
282
273
|
|
|
283
274
|
newPlaylist = {
|
|
284
275
|
name,
|
|
285
|
-
type: playlistType.value
|
|
276
|
+
type: playlistType.value,
|
|
286
277
|
};
|
|
287
278
|
}
|
|
288
279
|
|
|
280
|
+
const target = getTarget(newPlaylist?.name);
|
|
281
|
+
|
|
289
282
|
try {
|
|
290
283
|
emit('save-video-to-playlist', {
|
|
291
284
|
form: postVideoForm.value as IPlaylistVideoFormType,
|
|
@@ -293,23 +286,17 @@ async function onSaveClick() {
|
|
|
293
286
|
newPlaylist,
|
|
294
287
|
});
|
|
295
288
|
closePopup();
|
|
296
|
-
show('
|
|
289
|
+
show('added_to', {
|
|
290
|
+
icon: 'playlist',
|
|
291
|
+
highlight: target.label,
|
|
292
|
+
actionText: 'see_list',
|
|
293
|
+
onAction: () => navigateTo(target.link),
|
|
294
|
+
});
|
|
297
295
|
} catch (error) {
|
|
298
296
|
setErrorState(error);
|
|
299
297
|
console.log(error, 'error add video to playlist');
|
|
300
298
|
}
|
|
301
299
|
}
|
|
302
|
-
|
|
303
|
-
watch(
|
|
304
|
-
playlistsItems,
|
|
305
|
-
(items) => {
|
|
306
|
-
if (!items?.length) return;
|
|
307
|
-
if (selectValue.value.length) return;
|
|
308
|
-
|
|
309
|
-
selectValue.value = ['favorites'];
|
|
310
|
-
},
|
|
311
|
-
{ immediate: true }
|
|
312
|
-
);
|
|
313
300
|
</script>
|
|
314
301
|
|
|
315
302
|
<style lang="scss">
|
|
@@ -319,6 +306,13 @@ watch(
|
|
|
319
306
|
}
|
|
320
307
|
}
|
|
321
308
|
|
|
309
|
+
/* Ширина фиксирована: длинное название плейлиста обрезается, а не растягивает модалку. */
|
|
310
|
+
.playlist-add .ui-popup__wrapper {
|
|
311
|
+
@include from-br(sm) {
|
|
312
|
+
width: var(--popup-min-width);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
322
316
|
.playlist-add .ui-popup__back {
|
|
323
317
|
@include from-br(sm) {
|
|
324
318
|
display: none;
|
|
@@ -329,8 +323,15 @@ watch(
|
|
|
329
323
|
padding: 0;
|
|
330
324
|
}
|
|
331
325
|
|
|
326
|
+
.playlist-add .ui-popup__footer {
|
|
327
|
+
padding-inline: 24px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* minmax(0, 1fr), а не дефолтный auto-трек: иначе трек растёт до max-content и длинное
|
|
331
|
+
название плейлиста делает строку шире модалки вместо обрезки. */
|
|
332
332
|
.playlist-add__wrapper {
|
|
333
333
|
display: grid;
|
|
334
|
+
grid-template-columns: minmax(0, 1fr);
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
.playlist-add__item {
|
|
@@ -345,6 +346,15 @@ watch(
|
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
|
|
349
|
+
/* Два класса не случайно: `.ui-checkbox` едет отдельным CSS-чанком и с одним классом
|
|
350
|
+
перебивает display по порядку загрузки. */
|
|
351
|
+
.playlist-add .playlist-add__item {
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
column-gap: 12px;
|
|
356
|
+
}
|
|
357
|
+
|
|
348
358
|
.playlist-add__item:not(:last-child) {
|
|
349
359
|
border-bottom: 1px solid $playlist-add-item-border;
|
|
350
360
|
}
|
|
@@ -357,10 +367,6 @@ watch(
|
|
|
357
367
|
min-width: 0;
|
|
358
368
|
}
|
|
359
369
|
|
|
360
|
-
.playlist-add__item-wrapper.--favorites {
|
|
361
|
-
text-transform: capitalize;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
370
|
.ui-checkbox__label .playlist-add__item-wrapper {
|
|
365
371
|
@include font-base;
|
|
366
372
|
|
|
@@ -372,6 +378,7 @@ watch(
|
|
|
372
378
|
justify-content: space-between;
|
|
373
379
|
align-items: center;
|
|
374
380
|
width: 100%;
|
|
381
|
+
min-width: 0;
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
.playlist-add__item:last-child {
|
|
@@ -410,7 +417,10 @@ watch(
|
|
|
410
417
|
}
|
|
411
418
|
|
|
412
419
|
.playlist-add__item.--new {
|
|
420
|
+
@include font-base;
|
|
421
|
+
|
|
413
422
|
color: $playlist-add-item-new;
|
|
423
|
+
font-weight: 500;
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
.playlist-add__item-edit {
|