itube-specs 0.0.821 → 0.0.823

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.
@@ -14,12 +14,19 @@
14
14
  @ended="emit('end')"
15
15
  @advancerequest="onAdvanceRequest"
16
16
  @sourcechange="onSourceChange"
17
+ @menurequest="onMenuRequest"
18
+ />
19
+
20
+ <!-- Внешнее меню плеера в нашем ui-popup (только мобила — menus: 'external-mobile'). -->
21
+ <PlayerMenu
22
+ v-model:open="menuOpen"
23
+ :request="menuRequest"
17
24
  />
18
25
  </template>
19
26
 
20
27
  <script setup lang="ts">
21
28
  import { ITubePlayer } from 'itube-modern-player/vue';
22
- import type { Player, PlayerOptions, VideoSource, LocaleCode, AdsOptions, PreviewMetaItem, SceneGroup } from 'itube-modern-player';
29
+ import type { Player, PlayerOptions, VideoSource, LocaleCode, AdsOptions, PreviewMetaItem, SceneGroup, MenuRequestPayload } from 'itube-modern-player';
23
30
  import { AdvertsHelper } from '../../utils/adverts-helper';
24
31
  import { AdSpotType, ThumbSize } from '../../runtime';
25
32
  import type { IVideoData, IVideoCard } from '../../types';
@@ -56,6 +63,16 @@ const { t, n, locale } = useI18n();
56
63
 
57
64
  const playerRef = ref<InstanceType<typeof ITubePlayer> & { player: Player | null }>();
58
65
 
66
+ // Внешнее меню (menus: 'external-mobile'): по нажатию меню-кнопки плеер эмитит модель,
67
+ // а рисуем её мы (PlayerMenu → ui-popup). menurequest прилетает на каждое нажатие.
68
+ const menuRequest = shallowRef<MenuRequestPayload | null>(null);
69
+ const menuOpen = ref(false);
70
+
71
+ function onMenuRequest(request: MenuRequestPayload): void {
72
+ menuRequest.value = request;
73
+ menuOpen.value = true;
74
+ }
75
+
59
76
  // Плеер эмитит advancerequest 'ended' только при включённом тумблере Autoplay, а на
60
77
  // кнопку next — 'next'. Значит play после свапа нужен ТОЛЬКО когда переход инициировали
61
78
  // мы (next / автопереход), а не когда source пересобрался по другой причине (сменилось
@@ -219,6 +236,8 @@ const options = computed<Omit<PlayerOptions, 'source'>>(() => ({
219
236
  },
220
237
  pauseScreen: false,
221
238
  language: playerLanguage.value,
239
+ // Десктоп — нативные попапы плеера; мобила (≤767px) — эмитит menurequest, рисуем сами.
240
+ menus: 'external-mobile',
222
241
  controls: {
223
242
  seekButtons: { label: (seconds) => String(seconds) },
224
243
  playlistButton: false,
@@ -263,27 +282,4 @@ async function loadAdConfig(): Promise<AdsOptions | undefined> {
263
282
  border-radius: 0;
264
283
  }
265
284
  }
266
-
267
- // Хост (.player-core) уже клипует по $radius-lg — снимаем собственный radius заглушки
268
- // пакета (inline 8px), иначе двойное скругление даёт чёрные уголки на свопе.
269
- .player-core .imp-player--lazy {
270
- border-radius: 0 !important;
271
- }
272
-
273
- // Пиксель-парити SSR-заглушки с реальной центральной кнопкой. Пакет форсит
274
- // inlineStyles:true (84px, --imp-accent-цвет), а прокинуть наши размеры/цвет через
275
- // styling нельзя — styling.themeColor перебил бы наш --imp-accent на живом плеере.
276
- // Поэтому бьём инлайн !important'ом (совпадает с .imp-center-btn--play в main.vue).
277
- .player-core .imp-player--lazy .imp-poster__play {
278
- width: 80px !important;
279
- height: 80px !important;
280
- padding: 22px !important;
281
- color: $video-play-btn-icon !important;
282
- background: $video-play-btn-bg !important;
283
- }
284
-
285
- .player-core .imp-player--lazy .imp-poster__play svg {
286
- width: 100%;
287
- height: 100%;
288
- }
289
285
  </style>
@@ -131,410 +131,4 @@ const { nextVideo, goNext } = useNextVideo({
131
131
  grid-row: 2;
132
132
  }
133
133
  }
134
-
135
- // Оверрайды иконок/кнопок плеера (itube-modern-player). Держим здесь, а не в
136
- // ленивом core.vue: CSS пакета грузится по интеракции, наши стили в бандле страницы
137
- // присутствуют до неё и выигрывают в каскаде сразу (без вспышки дефолтного акцента).
138
- .player-main__player .imp-player {
139
- --imp-accent: #{$video-status-accent};
140
- }
141
-
142
- // Центральная и постерная play-кнопки — один в один (постерную показывает плеер
143
- // во время load(), при свопе с центральной был скачок размера/паддинга).
144
- // Локальный --imp-accent = наш цвет: пакет рисует фон через var(--imp-accent) и
145
- // анимирует его (transition: background) — совпадение значений убирает вспышку.
146
- .player-main__player .imp-center-btn.imp-center-btn--play,
147
- .player-main__player .imp-poster .imp-poster__play {
148
- $size: 80px;
149
-
150
- --imp-accent: #{$video-play-btn-bg};
151
-
152
- width: $size;
153
- height: $size;
154
- padding: 22px;
155
- color: $video-play-btn-icon;
156
- background: $video-play-btn-bg;
157
-
158
- @include hover(background) {
159
- background: $video-play-btn-bg--hover;
160
- }
161
- }
162
-
163
- .player-main__player .imp-btn.imp-btn--play {
164
- @include hover(color) {
165
- color: $video-play-console-play-btn-bg--hover;
166
- background-color: transparent;
167
- }
168
- }
169
-
170
- .player-main__player button.imp-btn {
171
- color: $video-play-console-btn-bg;
172
-
173
- @include hover(color) {
174
- background-color: transparent;
175
- color: $video-play-console-btn-bg--hover;
176
- }
177
- }
178
-
179
- .player-main__player .imp-btn.imp-btn--next {
180
- padding: 8px;
181
- }
182
-
183
- // Последний ролик последней страницы related — кнопки перехода на следующий скрыты.
184
- .player-main__player.--playlist-end .imp-btn--next,
185
- .player-main__player.--playlist-end .imp-center-btn--next {
186
- display: none;
187
- }
188
-
189
- // Все кнопки нижнего бара (кроме play) — иконка 20px. Без этого svg тянется на 100%
190
- // кнопки (правило пакета .imp-btn svg).
191
- .player-main__player .imp-btn:not(.imp-btn--play, .imp-poster__play) svg {
192
- width: 20px;
193
- height: 20px;
194
- }
195
-
196
- // Шестерёнка настроек: когда открыто её меню (рядом появляется .imp-menu--wide) —
197
- // поворачиваем иконку на 45°. transform у .imp-btn уже с transition в пакете.
198
- .player-main__player .imp-btn--settings svg {
199
- display: block;
200
- transform-box: fill-box;
201
- transform-origin: center;
202
- transition: var(--transition) transform;
203
- }
204
-
205
- .player-main__player .imp-btn--settings:has(~ .imp-menu--wide) svg {
206
- transform: rotate(45deg);
207
- }
208
-
209
- // Перемотка назад — та же иконка, что и вперёд, отражённая по горизонтали.
210
- // Зеркалим только svg, число .imp-seek-num (сосед svg) остаётся читаемым.
211
- .player-main__player .imp-btn--seek-back svg,
212
- .player-main__player .imp-center-btn--seek-back svg {
213
- transform: scaleX(-1);
214
- }
215
-
216
- // Число шага (15) — внутрь круга иконки, по центру. Кнопки делаем relative,
217
- // число позиционируем поверх svg абсолютно.
218
- .player-main__player .imp-btn--seek-back,
219
- .player-main__player .imp-btn--seek-forward,
220
- .player-main__player .imp-center-btn--seek-back,
221
- .player-main__player .imp-center-btn--seek-forward {
222
- position: relative;
223
- }
224
-
225
- .player-main__player .imp-center-btn.imp-center-btn--seek-back,
226
- .player-main__player .imp-center-btn.imp-center-btn--seek-forward {
227
- width: 48px;
228
- height: 48px;
229
- background: $video-play-btn-bg;
230
- border-radius: $radius-full;
231
-
232
- @include hover(background) {
233
- background: $video-play-btn-bg--hover;
234
- }
235
- }
236
-
237
- .player-main__player .imp-center-btn--seek-back svg,
238
- .player-main__player .imp-center-btn--seek-forward svg {
239
- width: 36px;
240
- height: 36px;
241
- }
242
-
243
- .player-main__player .imp-btn--seek-back .imp-seek-num,
244
- .player-main__player .imp-btn--seek-forward .imp-seek-num,
245
- .player-main__player .imp-center-btn--seek-back .imp-seek-num,
246
- .player-main__player .imp-center-btn--seek-forward .imp-seek-num {
247
- position: absolute;
248
- top: 50%;
249
- left: 50%;
250
- font-size: 10px;
251
- font-weight: 700;
252
- transform: translate(-50%, -50%);
253
- }
254
-
255
- .player-main__player .imp-controls__group {
256
- column-gap: 8px;
257
- }
258
-
259
- .player-main__player .imp-next-preview {
260
- width: 320px;
261
- bottom: 64px;
262
- left: 64px;
263
- padding: 0;
264
- overflow: hidden;
265
- border: 1px solid $video-play-preview-border;
266
- border-radius: $radius-xl;
267
- background-color: $video-play-preview-bg;
268
- backdrop-filter: blur($blur-md);
269
- transform-origin: bottom left;
270
- animation: next-preview-in 200ms ease;
271
-
272
- // Скрим над «NEXT VIDEO» — читабельность поверх светлой обложки (kicker выше по DOM).
273
- &::before {
274
- content: '';
275
- position: absolute;
276
- top: 0;
277
- left: 0;
278
- right: 0;
279
- height: 48px;
280
- z-index: $z-index-plus;
281
- background: linear-gradient(to bottom, $video-play-preview-scrim, transparent);
282
- }
283
- }
284
-
285
- .player-main__player .imp-next-preview__kicker {
286
- @include font-3xs;
287
-
288
- position: absolute;
289
- top: 8px;
290
- left: 12px;
291
- color: $video-play-preview-title;
292
- font-weight: 600;
293
- letter-spacing: var(--tracking-widest);
294
- z-index: $z-index-plus;
295
- }
296
-
297
- .player-main__player .imp-next-preview__duration {
298
- @include font-3xs;
299
-
300
- bottom: 8px;
301
- right: 8px;
302
- padding: 2px 6px;
303
- color: $video-play-preview-title;
304
- background: $video-play-preview-duration-bg;
305
- border-radius: $radius-small;
306
- font-weight: 500;
307
- font-variant-numeric: tabular-nums;
308
- }
309
-
310
- .player-main__player .imp-next-preview__thumb {
311
- border-radius: 0;
312
- border-radius: $radius-xl $radius-xl 0 0;
313
- }
314
-
315
- .player-main__player .imp-next-preview__title {
316
- @include font-sm;
317
-
318
- display: -webkit-box;
319
- -webkit-box-orient: vertical;
320
- -webkit-line-clamp: 2;
321
- overflow: hidden;
322
- font-weight: 600;
323
- padding: 12px 12px 0;
324
- margin-top: 0;
325
- color: $video-play-preview-title;
326
- }
327
-
328
- .player-main__player .imp-next-preview__meta {
329
- @include font-xs;
330
-
331
- display: flex;
332
- align-items: center;
333
- gap: 6px;
334
- opacity: 1;
335
- padding: 0 12px 12px;
336
- margin-top: 6px;
337
- color: $video-play-preview-meta;
338
- }
339
-
340
- // Аватар канала — кружок 16px (svg-круг из previewMeta), цвет отдельным токеном.
341
- .player-main__player .imp-next-preview__meta-icon {
342
- width: 16px;
343
- height: 16px;
344
- margin-right: 6px;
345
- vertical-align: middle;
346
- color: $video-play-preview-avatar-bg;
347
- }
348
-
349
- .player-main__player .imp-next-preview__meta-icon svg {
350
- width: 100%;
351
- height: 100%;
352
- }
353
-
354
- // Разделитель чанков meta: пакет рисует «·», нам нужен «|». Рисуем прямоугольником,
355
- // а не символом — глиф «|» позиционируется шрифтом и всегда чуть смещён. margin слева;
356
- // справа отступ даёт flex-gap контейнера — так «|» ровно по центру между чанками.
357
- .player-main__player .imp-next-preview__meta-chunk:not(:last-child)::after {
358
- content: '';
359
- display: inline-block;
360
- width: 1px;
361
- height: 0.85em;
362
- margin-left: 6px;
363
- vertical-align: middle;
364
- background: $video-play-preview-separator;
365
- }
366
-
367
- @keyframes next-preview-in {
368
- from {
369
- opacity: 0;
370
- transform: scale(0.95);
371
- }
372
- }
373
-
374
- .player-main__player .imp-controls__time {
375
- @include font-xs;
376
- }
377
-
378
- .player-main__player .imp-layer__bottom {
379
- padding: 32px 16px 10px;
380
- }
381
-
382
- .player-main__player .imp-controls {
383
- gap: 0;
384
- }
385
-
386
- .player-main__player .imp-menu--wide .imp-menu__item:not(.imp-menu__back, .imp-menu__toggle) {
387
- justify-content: center;
388
- }
389
-
390
- .player-main__player .imp-menu__back {
391
- color: $video-play-menu-back-text;
392
- margin-bottom: 0;
393
- border: none;
394
- }
395
-
396
- .player-main__player .imp-btn.imp-btn--scene-types {
397
- @include to-br(sm) {
398
- width: auto;
399
- }
400
- }
401
-
402
- .player-main__player .imp-menu {
403
- min-width: 224px;
404
- color: $video-play-menu-label;
405
- padding: 0;
406
- background-color: $video-play-menu-bg;
407
- border: 1px solid $video-play-menu-border;
408
- border-radius: $radius-xl;
409
- backdrop-filter: blur($blur-md);
410
- box-shadow: $video-play-menu-shadow;
411
-
412
- @include to-br(sm) {
413
- bottom: 4px;
414
- max-height: unset;
415
- }
416
-
417
- @include from-br(sm) {
418
- max-height: 400px;
419
- }
420
- }
421
-
422
- .player-main__player .imp-menu__title {
423
- display: none;
424
- }
425
-
426
- // Заголовок показываем только у меню типов сцен (единственное с иконками пунктов;
427
- // у gear-меню строки без .imp-menu__icon). Стиль — под дизайн, а не дефолтный uppercase.
428
- .player-main__player .imp-menu:has(.imp-menu__icon) .imp-menu__title {
429
- @include font-sm;
430
-
431
- display: block;
432
- padding: 12px;
433
- color: $video-play-menu-label;
434
- font-weight: 500;
435
- letter-spacing: normal;
436
- text-transform: none;
437
- opacity: 1;
438
- }
439
-
440
- // Иконка типа сцен в пункте меню — 16px (как иконки по проекту), цвет по currentColor.
441
- .player-main__player .imp-menu__icon {
442
- width: 16px;
443
- height: 16px;
444
- }
445
-
446
- .player-main__player .imp-menu__label {
447
- color: $video-play-menu-label;
448
- }
449
-
450
- .player-main__player .imp-menu__row.imp-menu__value {
451
- color: $video-play-menu-value;
452
- opacity: 1;
453
- }
454
-
455
- .player-main__player .imp-menu__row .imp-menu__chevron {
456
- margin-left: 0;
457
- color: $video-play-menu-chevron;
458
- opacity: 1;
459
- }
460
-
461
- .player-main__player .imp-menu__row {
462
- column-gap: 4px;
463
- }
464
-
465
- .player-main__player .imp-menu__item {
466
- padding: 12px 16px;
467
- border-radius: 0;
468
- color: $video-play-item-text;
469
-
470
- @include hover(background-color) {
471
- background-color: $video-play-item--hover;
472
- }
473
- }
474
-
475
- .player-main__player .imp-menu__item.imp-menu__item--active {
476
- position: relative;
477
- color: $video-play-item-text--active;
478
- background-color: $video-play-item--active;
479
-
480
- &::after {
481
- content: '';
482
- position: absolute;
483
- top: 50%;
484
- transform: translateY(-50%);
485
- width: 2px;
486
- height: 20px;
487
- right: 0;
488
- border-radius: $radius-md;
489
- background-color: $video-play-item-active-decorate;
490
- }
491
- }
492
-
493
- .player-main__player .imp-progress__tooltip {
494
- display: grid;
495
- grid-template-columns: 1fr auto;
496
- padding: 0;
497
- border-radius: $radius-xl;
498
- overflow: hidden;
499
- border: 1px solid $video-play-progress-tooltip-border;
500
- background-color: $video-play-progress-tooltip-bg;
501
- backdrop-filter: blur($blur-md);
502
- }
503
-
504
- // Размер тумбы задаёт пакет инлайном по тайлу VTT — фикс-ширину/aspect не навязываем
505
- // (иначе тумба вылезает); только не даём выйти за тултип.
506
- .player-main__player .imp-progress__thumb {
507
- grid-column: 1/-1;
508
- max-width: 100%;
509
- }
510
-
511
- // Полоса подписи (chapter/time) — пакет для --thumb даёт ей свой фон #26262d.
512
- // Совмещаем с фоном карточки, чтобы поверхность была единой, как в дизайне.
513
- .player-main__player .imp-progress__tooltip--thumb .imp-progress__caption {
514
- background: $video-play-progress-tooltip-bg;
515
- }
516
-
517
- .player-main__player .imp-progress__tooltip-chapter {
518
- @include font-3xs;
519
-
520
- grid-column: 1;
521
- min-width: 0;
522
- padding: 6px 8px;
523
- font-weight: 500;
524
- letter-spacing: 0.025em;
525
- }
526
-
527
- .player-main__player .imp-progress__tooltip-time {
528
- @include font-3xs;
529
-
530
- grid-column: 2;
531
- justify-self: end;
532
- padding: 6px 8px;
533
- font-weight: 600;
534
- font-variant-numeric: tabular-nums;
535
- }
536
-
537
- .player-main__player .imp-progress__track {
538
- column-gap: 4px;
539
- }
540
134
  </style>
@@ -0,0 +1,406 @@
1
+ <!-- Внешнее меню плеера (menus: 'external-mobile'): плеер на мобиле не рисует свои
2
+ попапы, а эмитит menurequest с готовой моделью (настройки / тайм-коды / ⋯) и живыми
3
+ колбэками, которые сами применяют выбор. Рендерим её в нашем ui-popup (--sheet).
4
+ Дрилл «Настройки → Скорость/Качество» — через стек drill с кнопкой «назад». -->
5
+ <template>
6
+ <UiPopup
7
+ v-if="open && request"
8
+ v-model="open"
9
+ sheet
10
+ :back="!!drill"
11
+ class="player-menu"
12
+ @back="drill = null"
13
+ >
14
+ <template #title>
15
+ {{ drill?.label ?? request.title }}
16
+ </template>
17
+
18
+ <!-- Дрилл: опции одной настройки (скорость / качество / субтитры) -->
19
+ <ul
20
+ v-if="drill"
21
+ class="player-menu__options"
22
+ >
23
+ <li
24
+ v-for="option in drill.options"
25
+ :key="option.value"
26
+ >
27
+ <button
28
+ :class="{ '--active': option.active }"
29
+ class="player-menu__option"
30
+ type="button"
31
+ @click="pick(drill.select, option.value)"
32
+ >
33
+ {{ option.label }}
34
+ </button>
35
+ </li>
36
+ </ul>
37
+
38
+ <!-- Шестерёнка: тоглы + строки-дриллы -->
39
+ <ul
40
+ v-else-if="request.kind === 'settings'"
41
+ class="player-menu__rows"
42
+ >
43
+ <li
44
+ v-for="entry in request.entries"
45
+ :key="entry.key"
46
+ class="player-menu__row"
47
+ >
48
+ <div
49
+ v-if="entry.toggle"
50
+ class="player-menu__toggle-row"
51
+ >
52
+ <span class="player-menu__label">{{ entry.label }}</span>
53
+ <UiToggle
54
+ :model-value="entry.toggle.value"
55
+ @update:model-value="entry.toggle.set(!!$event)"
56
+ />
57
+ </div>
58
+ <button
59
+ v-else-if="entry.options && entry.select"
60
+ class="player-menu__row-btn"
61
+ type="button"
62
+ @click="drill = { label: entry.label, options: entry.options, select: entry.select }"
63
+ >
64
+ <span class="player-menu__label">{{ entry.label }}</span>
65
+ <span class="player-menu__value">
66
+ {{ entry.value }}
67
+ <UiIcon
68
+ name="angle-right"
69
+ size="16"
70
+ />
71
+ </span>
72
+ </button>
73
+ <div
74
+ v-else
75
+ class="player-menu__toggle-row"
76
+ >
77
+ <span class="player-menu__label">{{ entry.label }}</span>
78
+ <span class="player-menu__value">{{ entry.value }}</span>
79
+ </div>
80
+ </li>
81
+ </ul>
82
+
83
+ <!-- Тайм-коды: аккордеон типов сцен -->
84
+ <ul
85
+ v-else-if="request.kind === 'sceneTypes'"
86
+ class="player-menu__groups"
87
+ >
88
+ <li
89
+ v-for="group in request.groups"
90
+ :key="group.id"
91
+ >
92
+ <details
93
+ :open="group.active"
94
+ class="player-menu__group"
95
+ @toggle="onGroupToggle($event, group)"
96
+ >
97
+ <summary class="player-menu__summary">
98
+ <span
99
+ v-if="iconSvg(group.icon)"
100
+ class="player-menu__summary-icon"
101
+ v-html="iconSvg(group.icon)"
102
+ />
103
+ <span class="player-menu__summary-title">{{ group.title }}</span>
104
+ <UiIcon
105
+ name="angle-right"
106
+ size="16"
107
+ class="player-menu__summary-arrow"
108
+ />
109
+ </summary>
110
+ <ul class="player-menu__scenes">
111
+ <li
112
+ v-for="scene in group.scenes"
113
+ :key="scene.start"
114
+ >
115
+ <button
116
+ :class="{ '--current': scene.current }"
117
+ class="player-menu__scene"
118
+ type="button"
119
+ @click="selectScene(group.id, scene.start)"
120
+ >
121
+ <span class="player-menu__scene-label">{{ scene.label }}</span>
122
+ <span class="player-menu__scene-time">{{ scene.time }}</span>
123
+ </button>
124
+ </li>
125
+ </ul>
126
+ </details>
127
+ </li>
128
+ </ul>
129
+
130
+ <!-- ⋯ и одиночные меню (скорость/качество/субтитры, если вынесены кнопками) -->
131
+ <template v-else-if="request.kind === 'menu'">
132
+ <section
133
+ v-for="(section, index) in request.sections"
134
+ :key="index"
135
+ class="player-menu__section"
136
+ >
137
+ <h3
138
+ v-if="section.title"
139
+ class="player-menu__section-title"
140
+ >
141
+ {{ section.title }}
142
+ </h3>
143
+ <ul class="player-menu__options">
144
+ <li
145
+ v-for="item in section.items"
146
+ :key="item.value"
147
+ >
148
+ <button
149
+ :class="{ '--active': item.active }"
150
+ class="player-menu__option"
151
+ type="button"
152
+ @click="pick(section.select, item.value)"
153
+ >
154
+ {{ item.label }}
155
+ </button>
156
+ </li>
157
+ </ul>
158
+ </section>
159
+ </template>
160
+ </UiPopup>
161
+ </template>
162
+
163
+ <script setup lang="ts">
164
+ import type { MenuRequestPayload } from 'itube-modern-player';
165
+
166
+ type SceneGroup = Extract<MenuRequestPayload, { kind: 'sceneTypes' }>['groups'][number];
167
+ type SettingsEntry = Extract<MenuRequestPayload, { kind: 'settings' }>['entries'][number];
168
+
169
+ // Иконку типа сцен рендерим только когда это сырой svg (мы отдаём именно {svg} из core.vue).
170
+ function iconSvg(icon?: SceneGroup['icon']): string | undefined {
171
+ return icon && typeof icon === 'object' && 'svg' in icon ? icon.svg : undefined;
172
+ }
173
+
174
+ interface Drill {
175
+ label: string
176
+ options: NonNullable<SettingsEntry['options']>
177
+ select: (value: string) => void
178
+ }
179
+
180
+ const props = defineProps<{
181
+ request: MenuRequestPayload | null
182
+ }>();
183
+
184
+ const open = defineModel<boolean>('open', { required: true });
185
+
186
+ // Стек дрилла «Настройки → опция». Сбрасываем на каждый новый запрос (плеер эмитит
187
+ // menurequest на каждое нажатие кнопки — открываем корень меню заново).
188
+ const drill = shallowRef<Drill | null>(null);
189
+ watch(() => props.request, () => {
190
+ drill.value = null;
191
+ });
192
+
193
+ // select у плеера сам делает setRate/seek/play и шлёт свои события — руками ничего не
194
+ // дёргаем, просто закрываем шторку.
195
+ function pick(select: (value: string) => void, value: string): void {
196
+ select(value);
197
+ open.value = false;
198
+ }
199
+
200
+ function selectScene(groupId: string, start: number): void {
201
+ if (props.request?.kind !== 'sceneTypes') return;
202
+ props.request.select(groupId, start);
203
+ open.value = false;
204
+ }
205
+
206
+ // Раскрытие группы = смена активного типа сцен (native ведёт себя так же).
207
+ function onGroupToggle(event: Event, group: SceneGroup): void {
208
+ if (props.request?.kind !== 'sceneTypes') return;
209
+ if ((event.target as HTMLDetailsElement).open && !group.active) {
210
+ props.request.activate(group.id);
211
+ }
212
+ }
213
+ </script>
214
+
215
+ <style lang="scss">
216
+ // Фон шторки под нативную (bg-zinc-900); шапка/бордеры ui-popup уже совпадают (#0d0d0d, zinc-800).
217
+ .player-menu .ui-popup__wrapper,
218
+ .player-menu .ui-popup__content {
219
+ background-color: $player-menu-bg;
220
+ }
221
+
222
+ .player-menu__rows,
223
+ .player-menu__groups,
224
+ .player-menu__options {
225
+ display: flex;
226
+ flex-direction: column;
227
+ }
228
+
229
+ .player-menu__row + .player-menu__row {
230
+ border-top: 1px solid $player-menu-row-border;
231
+ }
232
+
233
+ .player-menu__toggle-row,
234
+ .player-menu__row-btn {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: space-between;
238
+ gap: 16px;
239
+ width: 100%;
240
+ min-height: 52px;
241
+ padding: 8px 20px;
242
+ }
243
+
244
+ .player-menu__row-btn {
245
+ @include hover(background-color) {
246
+ background-color: $player-menu-row-bg--hover;
247
+ }
248
+ }
249
+
250
+ .player-menu__label {
251
+ @include font-sm;
252
+
253
+ color: $player-menu-row-text;
254
+ }
255
+
256
+ .player-menu__value {
257
+ @include font-sm;
258
+
259
+ display: inline-flex;
260
+ align-items: center;
261
+ gap: 4px;
262
+ color: $player-menu-value;
263
+ }
264
+
265
+ .player-menu__option {
266
+ @include font-sm;
267
+
268
+ position: relative;
269
+ display: flex;
270
+ align-items: center;
271
+ width: 100%;
272
+ min-height: 48px;
273
+ padding: 8px 20px;
274
+ color: $player-menu-option-text;
275
+
276
+ @include hover(background-color) {
277
+ background-color: $player-menu-option-bg--hover;
278
+ }
279
+ }
280
+
281
+ .player-menu__option.--active {
282
+ background-color: $player-menu-option-bg--active;
283
+ color: $player-menu-option-text--active;
284
+ font-weight: 500;
285
+ }
286
+
287
+ .player-menu__option.--active::before {
288
+ content: '';
289
+ position: absolute;
290
+ inset-block: 0;
291
+ left: 0;
292
+ width: 2px;
293
+ background-color: $player-menu-option-bar--active;
294
+ }
295
+
296
+ .player-menu__groups > li + li {
297
+ border-top: 1px solid $player-menu-acc-border;
298
+ }
299
+
300
+ .player-menu__summary {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 10px;
304
+ min-height: 52px;
305
+ padding: 13px 20px;
306
+ color: $player-menu-acc-head-text;
307
+ cursor: pointer;
308
+ list-style: none;
309
+ user-select: none;
310
+
311
+ &::-webkit-details-marker {
312
+ display: none;
313
+ }
314
+ }
315
+
316
+ .player-menu__group[open] .player-menu__summary {
317
+ color: $player-menu-acc-head-text--open;
318
+ }
319
+
320
+ .player-menu__summary-icon {
321
+ display: grid;
322
+ place-content: center;
323
+ color: $player-menu-icon;
324
+
325
+ svg {
326
+ width: 20px;
327
+ height: 20px;
328
+ }
329
+ }
330
+
331
+ .player-menu__summary-title {
332
+ @include font-sm;
333
+
334
+ flex: 1;
335
+ overflow: hidden;
336
+ text-overflow: ellipsis;
337
+ white-space: nowrap;
338
+ }
339
+
340
+ .player-menu__summary-arrow {
341
+ flex-shrink: 0;
342
+ color: $player-menu-icon;
343
+ transition: transform var(--transition);
344
+ }
345
+
346
+ .player-menu__group[open] .player-menu__summary-arrow {
347
+ transform: rotate(90deg);
348
+ }
349
+
350
+ .player-menu__scene {
351
+ @include font-sm;
352
+
353
+ position: relative;
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ gap: 16px;
358
+ width: 100%;
359
+ min-height: 44px;
360
+ padding: 10px 20px 10px 48px;
361
+ color: $player-menu-scene-text;
362
+
363
+ @include hover(background-color, color) {
364
+ background-color: $player-menu-scene-bg--hover;
365
+ color: $player-menu-scene-text--current;
366
+ }
367
+ }
368
+
369
+ .player-menu__scene.--current {
370
+ background-color: $player-menu-scene-bg--current;
371
+ color: $player-menu-scene-text--current;
372
+ font-weight: 500;
373
+ box-shadow: inset 3px 0 $player-menu-scene-bar--current;
374
+ }
375
+
376
+ .player-menu__scene-label {
377
+ overflow: hidden;
378
+ text-overflow: ellipsis;
379
+ white-space: nowrap;
380
+ }
381
+
382
+ .player-menu__scene-time {
383
+ @include font-xs;
384
+
385
+ flex-shrink: 0;
386
+ font-variant-numeric: tabular-nums;
387
+ color: $player-menu-scene-time;
388
+ }
389
+
390
+ .player-menu__scene.--current .player-menu__scene-time {
391
+ color: $player-menu-scene-time--current;
392
+ }
393
+
394
+ .player-menu__section + .player-menu__section {
395
+ border-top: 1px solid $player-menu-row-border;
396
+ }
397
+
398
+ .player-menu__section-title {
399
+ @include font-xs;
400
+
401
+ padding: 12px 20px 4px;
402
+ text-transform: uppercase;
403
+ letter-spacing: var(--tracking-wider);
404
+ color: $player-menu-section-title;
405
+ }
406
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.821",
4
+ "version": "0.0.823",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {