itube-modern-player 0.8.4 → 0.8.5

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/README.md CHANGED
@@ -30,6 +30,7 @@
30
30
  - [События](#события)
31
31
  - [События: `PlayerEventMap`](#события-playereventmap)
32
32
  - [Vue 3: `<ITubePlayer>`](#vue-3-itubeplayer)
33
+ - [Меню в дровере приложения: `menus: 'external'`](#меню-в-дровере-приложения-menus-external)
33
34
  - [Экспортируемые типы](#экспортируемые-типы)
34
35
  - [Экспортируемые утилиты](#экспортируемые-утилиты)
35
36
  - [Темизация](#темизация)
@@ -377,6 +378,10 @@ new Player('#mount', {
377
378
  playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2], // пункты меню скорости
378
379
  seekStep: 15, // секунды для стрелок/кнопок перемотки (по умолчанию 15; показывается на кнопке)
379
380
  keyboard: true, // горячие клавиши (на контейнере, не на document!)
381
+ // Кто рендерит меню контрол-бара: 'native' (дефолт) | 'external' | 'external-mobile'.
382
+ // 'external*' — вместо открытия меню плеер эмитит `menurequest`, рендерит приложение
383
+ // (см. раздел «Меню в дровере приложения»).
384
+ menus: 'native',
380
385
  className: 'my-player', // свой класс на контейнер — хук для темизации
381
386
  crossOrigin: 'anonymous', // если VTT/постеры на другом домене
382
387
  playsInline: true,
@@ -430,6 +435,8 @@ new Player('#mount', {
430
435
  play: true,
431
436
  progress: true,
432
437
  time: true,
438
+ // Громкость (mute + слайдер): true — только десктоп (на мобиле скрыта,
439
+ // там аппаратные кнопки); 'always' — показывать и на мобиле; false — нигде.
433
440
  volume: true,
434
441
  fullscreen: true,
435
442
  pip: true,
@@ -441,8 +448,14 @@ new Player('#mount', {
441
448
  subtitles: 'gear', // субтитры; появляется только если у источника есть треки
442
449
  scenes: true, // кнопка списка сцен (появляется при наличии chapters)
443
450
  sceneTypes: true, // контрол выбора типа сцен (появляется при source.sceneGroups)
444
- heatmap: true, // диаграмма популярности (нужны данные source.heatmap)
451
+ // Хитмап популярности (нужны данные source.heatmap): true — только десктоп
452
+ // (на мобиле скрыт); 'always' — и на мобиле; false — нигде.
453
+ heatmap: true,
445
454
  seekButtons: true, // или { back: 5, forward: 15, label: (sec, dir) => `${dir==='back'?'−':'+'}${sec}s` }
455
+ // Меню «⋯» (экшены + динамический overflow-коллапс узкого бара).
456
+ // false — «⋯» нет вовсе (как в макете): контролы никогда не сворачиваются,
457
+ // а actions без placement: 'bar' не отображаются.
458
+ more: true,
446
459
  // порядок правых контролов слева направо; пусто/нет — встроенный порядок.
447
460
  // перечисленные идут первыми, остальные сохраняют дефолтную позицию, ⋯ всегда последняя.
448
461
  order: ['gear', 'pip', 'fullscreen'], // ControlBarItem[]: like|dislike|speed|quality|subtitles|gear|scenes|sceneTypes|playlist|pip|fullscreen|`custom:<id>`
@@ -698,6 +711,7 @@ player.off('timeupdate', fn)
698
711
  | `relatedclick` | `{ item }` | Клик по related-карточке |
699
712
  | `action` | `{ id }` | Кнопки: `like`, `dislike`, `addTo`, `share` (если нет нативного шеринга), `report` |
700
713
  | `customaction` | `{ id }` | Ваша кнопка из `actions.custom` |
714
+ | `menurequest` | `MenuRequestPayload` | Нажата кнопка меню при `menus: 'external' \| 'external-mobile'` — приложение рендерит меню само ([раздел «Меню в дровере приложения»](#меню-в-дровере-приложения-menus-external)) |
701
715
  | `adstart` / `adend` / `adskip` / `adclick` | `{ ad: ResolvedAd }` | Жизненный цикл рекламы |
702
716
  | `adpause` / `adresume` | `{ ad: ResolvedAd }` | Креатив поставлен на паузу / возобновлён (+ VAST-пиксели pause/resume) |
703
717
  | `aderror` | `{ roll, error }` | Ролл не отыграл (контент продолжается автоматически) |
@@ -725,6 +739,149 @@ player.off('timeupdate', fn)
725
739
 
726
740
  CSS компонент подгружает сам (self-reference `itube-modern-player/style.css` через exports пакета); ручной импорт `style.css` в приложении продолжает работать и дедуплицируется бандлером — это фолбэк для экзотических сборок.
727
741
 
742
+ ## Меню в дровере приложения: `menus: 'external'`
743
+
744
+ У контрол-бара три меню: **настройки** (шестерёнка: автоплей / скорость / качество / субтитры), **тайм-коды** (типы сцен с аккордеоном) и **⋯** (переполнившиеся контролы + ваши экшены). Кто их рисует — решает опция `menus`:
745
+
746
+ | Значение | Поведение |
747
+ | --- | --- |
748
+ | `'native'` (по умолчанию) | Плеер рисует сам. Десктоп — попап у кнопки; мобила — дровер **снизу экрана**: открытое меню вместе с затемнением переезжает в фиксированный слой на `<body>` (порталится оно потому, что сам плеер обрезает fixed-потомков), страница под ним затемняется и не скроллится. Поверх шапки сайта дровер поднимает `z-index: var(--imp-sheet-z, 999)` — при конфликте задайте `--imp-sheet-z` на `:root` |
749
+ | `'external'` | Плеер **никогда** не открывает меню — по нажатию любой меню-кнопки эмитит `menurequest`, рендер за вами |
750
+ | `'external-mobile'` | Гибрид для приложений со своим дровером: десктоп — нативные попапы, мобильный вьюпорт (≤767px) — `menurequest` |
751
+
752
+ ### Контракт `menurequest`
753
+
754
+ Один payload — всё, что нужно для рендера, плюс колбэки, которые **сами применяют выбор** (плеер перемотает/переключит и отправит свои обычные события — ничего дёргать вручную не надо):
755
+
756
+ ```ts
757
+ type MenuRequestPayload =
758
+ // Шестерёнка. Строки трёх видов: тогл (toggle), дрилл в опции (options+select), просто значение.
759
+ | { kind: 'settings'; title: string; entries: ExternalSettingsEntry[] }
760
+ // Тайм-коды: группы сцен; active — текущий тип (в макете он раскрыт).
761
+ | { kind: 'sceneTypes'; title: string; groups: ExternalSceneGroup[];
762
+ activate(groupId: string): void // раскрыли группу = сделали её активным типом
763
+ select(groupId: string, start: number): void } // тап по сцене: активирует группу, seek + play
764
+ // Всё остальное (⋯, отдельные кнопки скорости/качества/субтитров, если включены).
765
+ | { kind: 'menu'; title: string; sections: ExternalMenuSection[] }
766
+
767
+ interface ExternalSettingsEntry {
768
+ key: string // 'autoplay' | 'speed' | 'quality' | 'subtitles'
769
+ label: string // локализованная подпись
770
+ value?: string // текущее значение для дрилл-строки («720p», «Обычная»)
771
+ toggle?: { value: boolean; set(on: boolean): void }
772
+ options?: { label: string; value: string; active: boolean }[]
773
+ select?(value: string): void
774
+ }
775
+
776
+ interface ExternalSceneGroup {
777
+ id: string; title: string; icon?: IconSource
778
+ active: boolean // текущий тип сцен — подсветите/раскройте его
779
+ scenes: { label: string; time: string; start: number; current: boolean }[]
780
+ } // current — сцена, играющая сейчас (подсветка в макете)
781
+
782
+ interface ExternalMenuSection {
783
+ title: string
784
+ items: { label: string; value: string; active: boolean }[]
785
+ select(value: string): void
786
+ }
787
+ ```
788
+
789
+ Три правила, чтобы не отстрелить ногу:
790
+
791
+ 1. **Payload — снимок на момент нажатия.** Колбэки живые (замыкания на плеер), но `active`/`current`/`value` не обновляются сами. После `select`/`toggle` либо закрывайте дровер (обычный UX), либо обновите свою копию модели — по событиям плеера `ratechange`, `qualitychange`, `subtitlechange`, `scenetypechange`, `chapterchange`.
792
+ 2. **Не дублируйте работу колбэков.** `select` у `sceneTypes` уже делает `setSceneGroup + seek + play` — не вызывайте `player.seek()` сами.
793
+ 3. `menurequest` эмитится **на каждое нажатие** кнопки — если ваш дровер уже открыт, просто перерисуйте контент.
794
+
795
+ ### Интеграция в Nuxt 3 (со своим дровером)
796
+
797
+ Пусть в приложении уже есть `<AppDrawer v-model="open" :title="...">` (любой ваш bottom-sheet). Вся интеграция — один обработчик и один рендер по `kind`:
798
+
799
+ ```vue
800
+ <script setup lang="ts">
801
+ import type { MenuRequestPayload } from 'itube-modern-player'
802
+
803
+ const video = { src: '...', sceneGroups: [/* ... */] }
804
+
805
+ const drawerOpen = ref(false)
806
+ const menu = shallowRef<MenuRequestPayload | null>(null)
807
+ // стек для дрилла «Настройки → Скорость» с кнопкой «назад»
808
+ const drill = shallowRef<{ label: string; options: { label: string; value: string; active: boolean }[]; select(v: string): void } | null>(null)
809
+
810
+ function onMenuRequest(req: MenuRequestPayload) {
811
+ menu.value = req
812
+ drill.value = null
813
+ drawerOpen.value = true
814
+ }
815
+
816
+ function pick(select: (v: string) => void, value: string) {
817
+ select(value) // плеер применит сам и отправит свои события
818
+ drawerOpen.value = false
819
+ }
820
+ </script>
821
+
822
+ <template>
823
+ <ITubePlayer
824
+ lazy
825
+ :source="video"
826
+ :options="{ menus: 'external-mobile' }"
827
+ @menurequest="onMenuRequest"
828
+ />
829
+
830
+ <AppDrawer v-model="drawerOpen" :title="drill?.label ?? menu?.title">
831
+ <!-- дрилл: список опций одной настройки -->
832
+ <template v-if="drill">
833
+ <button v-for="o in drill.options" :key="o.value"
834
+ :class="{ active: o.active }"
835
+ @click="pick(drill.select, o.value)">
836
+ {{ o.label }}
837
+ </button>
838
+ </template>
839
+
840
+ <!-- корень шестерёнки -->
841
+ <template v-else-if="menu?.kind === 'settings'">
842
+ <template v-for="e in menu.entries" :key="e.key">
843
+ <label v-if="e.toggle">
844
+ {{ e.label }}
845
+ <AppSwitch :model-value="e.toggle.value" @update:model-value="e.toggle.set($event)" />
846
+ </label>
847
+ <button v-else @click="drill = { label: e.label, options: e.options!, select: e.select! }">
848
+ {{ e.label }} <span class="value">{{ e.value }}</span>
849
+ </button>
850
+ </template>
851
+ </template>
852
+
853
+ <!-- тайм-коды: аккордеон типов сцен -->
854
+ <template v-else-if="menu?.kind === 'sceneTypes'">
855
+ <details v-for="g in menu.groups" :key="g.id" :open="g.active"
856
+ @toggle="(e: any) => e.target.open && !g.active && menu!.activate(g.id)">
857
+ <summary>{{ g.title }}</summary>
858
+ <button v-for="s in g.scenes" :key="s.start"
859
+ :class="{ current: s.current }"
860
+ @click="menu!.select(g.id, s.start); drawerOpen = false">
861
+ {{ s.label }} <span class="time">{{ s.time }}</span>
862
+ </button>
863
+ </details>
864
+ </template>
865
+
866
+ <!-- ⋯ и одиночные меню -->
867
+ <template v-else-if="menu?.kind === 'menu'">
868
+ <section v-for="(s, i) in menu.sections" :key="i">
869
+ <h3 v-if="s.title">{{ s.title }}</h3>
870
+ <button v-for="it in s.items" :key="it.value"
871
+ :class="{ active: it.active }"
872
+ @click="pick(s.select, it.value)">
873
+ {{ it.label }}
874
+ </button>
875
+ </section>
876
+ </template>
877
+ </AppDrawer>
878
+ </template>
879
+ ```
880
+
881
+ Дизайн строк под макет (те же значения, что в нативном дровере): строки настроек — 52px / 15px, разделители `rgba(39,39,42,.6)`; опции — 48px, активная `rgba(255,255,255,.10)` + белый бар 2px слева во всю высоту; шапка — 18px/700 uppercase на `#0d0d0d`.
882
+
883
+ Ванилла-версия без Vue — то же самое через `player.on('menurequest', req => ...)`.
884
+
728
885
  ## Изоморфный шаблон заглушки: `itube-modern-player/placeholder`
729
886
 
730
887
  `renderPlaceholder(options, flags?) → string` — чистая строка без DOM-зависимостей, единый источник правды по разметке «постер + кнопка play». Используется внутри `createLazyPlayer` и Vue-`lazy`; экспортируется для SSR любого фреймворка:
@@ -890,6 +1047,46 @@ npm publish # prepublishOnly прогонит typecheck + build
890
1047
 
891
1048
  Версионирование по [SemVer](https://semver.org/lang/ru/): `major.minor.patch`.
892
1049
 
1050
+ ### 0.8.5
1051
+
1052
+ Дизайн-проход по макетам. Записи хронологические — макет уточнялся по ходу, поэтому при расхождении цифр (например, высота блока контролов 102 → 110px, зазоры 8 → 12px) верны **поздние** пункты; финальные значения также отражены в блоке опций выше.
1053
+
1054
+ - **Центр-кластер по макету**: play — тёмный круг 80px (accent-заливка убрана из дефолта; `playButtonStyle: 'inverted'` работает как раньше), кнопки ±N — тёмные круги 52px, **число шага внутри кольца** (голое «15» вместо подписи «−15с» под иконкой; кастомный `seekButtons.label` уважается).
1055
+ - **Контрол-бар**: иконка play 24px, остальные кнопки 20px (макетное соотношение); добавлен зазор между прогресс-баром и рядом кнопок.
1056
+ - **Затенение под контролами**: градиент выше и плотнее (`transparent → rgba(0,0,0,.85)`, старт с 48px) — бар читается на светлых кадрах.
1057
+ - **Хитмап по макету**: кривая с явным отступом над полосой прогресса (bottom 18px, высота 22px), профиль с высокой базовой линией (`FLOOR` 0.08 → 0.35) — толстая мягкая лента с холмами вместо острых пиков.
1058
+ - **Превью момента**: кадр на 15% меньше (`scale 0.85` от нативного спрайт-региона), полоска с названием сцены и временем — **под** кадром, а не поверх него; карточка с тонкой рамкой-инсетом.
1059
+ - **Меню таймкодов — аккордеон**: типы сцен как раскрывающиеся заголовки (иконка + название + шеврон), раскрытый тип показывает список сцен со временем начала; текущая сцена подсвечена (белый + accent-время), раскрытие типа делает его активным. Клик по сцене — переход + воспроизведение.
1060
+ - **Ползунок → подсветка позиции**: белая точка на прогресс-баре следует за курсором/пальцем (позиция наведения), а не за воспроизведением; ловится и по полосе, и по зоне хитмапа (хитмап теперь часть hit-области перемотки).
1061
+ - **Пиксель-перфект контрол-бара (десктоп)**: ряд кнопок 36px, отступы блока 32/16/10 — общая высота с хитмапом ровно 102px; хитмап ниже (лента 16px, зазор 16px).
1062
+ - **Превью момента**: кадр без полей — на всю ширину карточки; карточка поднята над хитмапом (не перекрывает его).
1063
+ - **Иконки**: play/next/prev со скруглёнными углами, pause с более круглыми плашками; новая иконка громкости (скруглённый динамик + дуги с round-cap).
1064
+ - **Меню настроек (шестерёнка) по макету**: карточка 224px, радиус 12px, тонкая обводка + глубокая тень, без внутреннего паддинга; строки 14px c ховером во всю ширину; свитч 40×24. Подменю (скорость/качество): заголовок «‹ Название», опции по центру, блеклый ховер, активная — светлая полоса + белая палочка у правого края.
1065
+ - **Шестерёнка** слегка проворачивается по часовой (35°) пока меню открыто.
1066
+ - **Прогресс-бар**: высота всегда 4px — по ховеру не утолщается, а подсвечивается мягким белым свечением.
1067
+ - **Синхронизация с дизайн-референсом**: кнопки бара подсвечиваются цветом (серый → белый, play → accent) вместо фоновой плашки; зазоры в ряду 8px; время 12px/500; градиент подложки `transparent → rgba(0,0,0,.7)`; центр-кластер — кольца 48px с зазорами 24px и полупрозрачными чёрными фонами; превью следующего видео 320px с pop-анимацией (fade + scale).
1068
+ - **Мобильные меню — bottom-sheet по референсу**: шапка на тёмной подложке `#0d0d0d` с центрированным заголовком (18px, uppercase) и кнопкой ✕, в подменю — стрелка «назад» в шапке; карточка `#18181b` со скруглённым верхом 16px и линией-гранью; тап-строки 52px с разделителями, опции 48px (активная — светлая полоса + 2px бар слева во всю высоту), свитчи 44×24; затемнение подложки до 70%.
1069
+ - Исправлен `labels.settings` во всех локалях: легаси «Скорость воспроизведения» → «Настройки» (шестерёнка давно открывает общее меню настроек).
1070
+ - **Мобильный дровер на уровне экрана**: открытое меню (+ затемнение) порталится из плеера в фиксированный слой на `<body>` — дровер выезжает из низа экрана и затемняет всю страницу, как в макете; скролл страницы блокируется, тема (accent и пр.) переносится на портал автоматически; z-index настраивается через `--imp-sheet-z`; в фуллскрине хостом становится fullscreen-элемент.
1071
+ - **`menus: 'external' | 'external-mobile'` + событие `menurequest`**: режим, в котором меню рендерит приложение (свой дровер в Nuxt и т.п.) — плеер отдаёт типизированную модель (настройки/тайм-коды/⋯) с колбэками, применяющими выбор. Полный контракт и Nuxt-пример — в разделе README «Меню в дровере приложения».
1072
+ - **Текущая сцена подсвечивается во всех типах**: позиция воспроизведения попадает в сцену каждой группы — в меню тайм-кодов (и в `menurequest`-модели) подсветка теперь считается по позиции для каждой группы, а не только для активной.
1073
+ - **Шрифты по макету**: заголовки аккордеона и подпись кнопки типа сцен 14px/500 (были 13/600), активная сцена — 500, ширина аккордеона 256px, время в тултипе превью 12px.
1074
+ - **Превью сика поверх всего**: карточка превью получила `z-index: 9` — перекрывает центр-кластер и остальные оверлеи (раньше кнопки центра рисовались поверх неё).
1075
+ - **Полоса прогресса не утолщается и на мобиле**: убран остаток «трек 10px под пальцем» — состояние скраба показывает то же белое свечение; тач-зона и увеличение точки-ползунка сохранены.
1076
+ - **Хитмап и громкость на мобиле скрыты по умолчанию**: `controls.heatmap` / `controls.volume` теперь `boolean | 'always'` — `true` (дефолт) = только десктоп, `'always'` = принудительно и на мобильном вьюпорте, `false` = нигде.
1077
+ - **Ховер-свечение прогресс-бара приглушено** до макетного (blur 3px / 35% вместо 8px / 45%).
1078
+ - **Постер-стейт = рабочий плеер**: до первого запуска бар сидируется полностью — время «0:00 / длительность» (из `source.duration` или метаданных), лейбл стартовой главы, интерактивный прогресс с главами/хитмапом/превью; кнопка play на постере теперь идентична центральной кнопке рабочего плеера (тёмный круг 80px; `playButtonStyle: 'inverted'` работает как раньше).
1079
+ - **Мобильная версия заново сверена с макетом**: шестерёнка — плавающая круглая кнопка 36px в правом верхнем углу видео (из бара убрана, аватар канала сдвигается левее, в ⋯ не коллапсится); кнопки бара 36px и зазоры 8px как на десктопе (компакт-уменьшение убрано); центр-кластер держит десктопные размеры (play 80 / кольца 48 / зазор 24) и центрируется в зоне НАД контрол-баром (десктоп −64px, мобила −40px снизу); подпись у кнопки типа сцен снова видна (паддинги 8px, иконка 16px); время 12px; отступы низа 32/16/12.
1080
+ - **Иконка шестерёнки** заменена на макетную — контурная с округлыми лепестками и кружком в центре (lucide settings); поворот при открытии меню работает как прежде.
1081
+ - **`controls.more: false`** — полное отключение меню «⋯» (как в макете): кнопка не рендерится, overflow-коллапс выключен, контролы никогда не сворачиваются. Дефолт — `true` (прежнее поведение).
1082
+ - Фикс: на мобиле кнопка настроек «прыгала» вниз к бару при показе контролов до старта — режим peek делал низ оверлея positioned, и абсолютная шестерёнка начинала позиционироваться от бара. Подъём над постером теперь через z-index grid-элемента без `position` (containing block не меняется), а центр-кластер до старта остаётся скрыт за постером — без задвоения кнопки play.
1083
+ - **Синхронизация с обновлённым макетом (июльская ревизия)**: кнопки контрол-бара 44×44 (иконки прежние 24/20), зазоры ряда 12px, общая высота блока 110px; иконки центр-кластера укрупнены до 36px (play на мобиле 64px с 28px-иконкой, постер-кнопка зеркалит); мобильная шестерёнка 40×40; градиент подложки по новым стопам `0 → .35@35% → .7@70% → .92`; у времени лёгкая тень для светлых кадров.
1084
+ - **Мобильный бар справа как в макете**: тип сцен — квадратная 44px кнопка только с иконкой (20px, подпись скрыта на мобиле), рядом фуллскрин. Заодно это вернуло фуллскрин на узких экранах — широкая кнопка с текстом выталкивала его за край плеера.
1085
+ - **Постерная кнопка play центрируется как кластерная** — в зоне над контрол-баром (−64px десктоп / −40px мобила), а не по всему плееру: при раскрытии контролов кнопка больше не «переезжает».
1086
+ - **Мобила: контрол-бар виден в заглушке сразу** — при показе постера бар раскрывается автоматически (без тапа) и не автоскрывается до первого запуска; на десктопе прежнее поведение (появление по ховеру).
1087
+ - Фикс «клика насквозь» у дровера: тап по оверлею над кнопкой (например, шестерёнкой) закрывал дровер, но клик проваливался в кнопку под ним и срабатывал (меню тут же переоткрывалось). Бэкдроп теперь закрывается по `click`, а не `pointerdown`, и поглощает касание целиком.
1088
+ - Фикс collapse-логики ⋯: замер переполнения больше не учитывает сам ⋯, оставшийся с прошлого прохода — уходит цикл «спрятали кнопку ради ⋯, который жив только из-за спрятанной кнопки».
1089
+
893
1090
  ### 0.8.4
894
1091
 
895
1092
  - **Прогресс на тепловой карте.** Кривая хитмапа больше не одноцветная: проигранная часть (до плейхеда) подсвечивается ярче (`rgba(255,255,255,.75)` против базовых `.35`; кастомизируется по классу `imp-progress__heatmap-played`). Заполнение следует и за воспроизведением, и за скрабом.