itube-specs 0.0.911 → 0.0.913

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.
Files changed (44) hide show
  1. package/assets/scss/base/_animations.scss +11 -10
  2. package/assets/scss/mixins/_font-sizes.scss +5 -2
  3. package/assets/scss/themes/dark/_theme.scss +22 -18
  4. package/assets/scss/themes/light/_theme.scss +17 -13
  5. package/assets/scss/vars/_tokens.scss +1 -1
  6. package/assets/scss/vars/_vars.scss +1 -0
  7. package/components/auth/register.vue +0 -4
  8. package/components/card/collections-more.vue +1 -1
  9. package/components/card/dropdown/index.vue +2 -2
  10. package/components/card/playlist.vue +7 -8
  11. package/components/card/video/actions.vue +2 -2
  12. package/components/card/video/index.vue +9 -9
  13. package/components/filter/popup.vue +1 -0
  14. package/components/library/feed.vue +1 -14
  15. package/components/playlist/add.vue +123 -113
  16. package/components/playlist/edit.vue +133 -199
  17. package/components/playlist/input.vue +23 -133
  18. package/components/playlist/main.vue +21 -40
  19. package/components/playlist/visibility.vue +149 -0
  20. package/components/section/collections.vue +2 -2
  21. package/components/section/swiper-cards.vue +3 -3
  22. package/components/ui/chips.vue +7 -2
  23. package/components/ui/confirm.vue +17 -5
  24. package/components/ui/dropdown.vue +17 -8
  25. package/components/ui/expand-row.vue +0 -4
  26. package/components/ui/popup.vue +33 -15
  27. package/components/ui/section-title.vue +6 -6
  28. package/components/ui/tooltip.vue +35 -14
  29. package/composables/__tests__/use-overlay-stack.test.ts +110 -0
  30. package/composables/__tests__/use-video-collections.test.ts +75 -0
  31. package/composables/use-overlay-stack.ts +58 -0
  32. package/composables/use-playlist-edit.ts +13 -3
  33. package/composables/use-snackbar.ts +15 -1
  34. package/composables/use-user.ts +0 -4
  35. package/composables/use-video-collections.ts +27 -5
  36. package/package.json +1 -1
  37. package/pages/following.vue +4 -4
  38. package/pages/history.vue +1 -1
  39. package/pages/library/index.vue +4 -4
  40. package/pages/playlists/index.vue +7 -76
  41. package/runtime/constants/playlist-type.ts +5 -0
  42. package/components/playlist/private-toggle.vue +0 -20
  43. package/composables/use-favorites.ts +0 -20
  44. 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="list"
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="16"
48
+ size="14"
73
49
  />
74
- <div class="playlist-add__item-icon">
50
+ <span class="playlist-add__item-icon">
75
51
  <UiIcon
76
52
  name="video-camera"
77
- size="16"
53
+ size="14"
78
54
  />
79
55
  {{ item.videosCount }}
80
- </div>
56
+ </span>
81
57
  </div>
82
58
  <button
83
59
  class="playlist-add__item-edit"
84
60
  type="button"
85
- @click.prevent="startEdit(item)"
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
- <template v-if="!editingId">
96
- <button
97
- v-if="step !== 'new'"
98
- type="button"
99
- class="playlist-add__item --new"
100
- @click="step = 'new'"
101
- >
102
- <UiIcon
103
- class="playlist-add__item-icon --new"
104
- name="plus"
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
- </template>
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="loadingUserPlaylists || loadingPostVideo"
108
+ :disabled="submitDisabled"
135
109
  size="l"
136
110
  theme="primary"
137
- @click="editingId ? saveEdit(editingId) : onSaveClick()"
138
- >{{ editingId ? $t('update') : $t('add') }}
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
- ?.filter((item: IPlaylistCard) => item.name !== FAVORITES_KEY)
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 === PlaylistType.Private ? 'lock' : 'user-group',
188
- playlistType: item.playlistType === PlaylistType.Private ? 'private' as const : 'public' as const,
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 = ['favorites'];
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 = 'public';
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<'public' | 'private'>('public');
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 = 'public';
206
+ playlistType.value = PlaylistType.Public;
226
207
  step.value = 'add';
227
208
  }
228
209
 
229
- const editingId = ref<string | null>(null);
230
- const editName = ref('');
231
- const editType = ref<'public' | 'private'>('public');
232
-
233
- function startEdit(item: { value: string, title: string, playlistType: 'public' | 'private' }) {
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 === 'private' ? PlaylistType.Private : PlaylistType.Public,
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('video_added');
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 {