itube-specs 0.0.1039 → 0.0.1041
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/DOCS/CONVENTIONS.md +30 -9
- package/SITE_UPDATES.md +79 -0
- package/components/adv/native-bottom.vue +0 -1
- package/components/adv/native-player.vue +0 -1
- package/components/adv/ntv.vue +0 -1
- package/components/grid/insert.vue +31 -0
- package/components/grid/models-end-slot.vue +16 -0
- package/components/grid/models.vue +21 -2
- package/components/grid/videos-end-slot.vue +16 -0
- package/components/grid/videos.vue +29 -4
- package/components/layout/burger.vue +3 -5
- package/components/layout/nav.vue +1 -2
- package/components/player/core.vue +30 -5
- package/components/view/video-after-player-slot.vue +13 -0
- package/components/view/video-after-related-slot.vue +13 -0
- package/components/view/video.vue +34 -5
- package/composables/page/use-category-page.ts +128 -0
- package/composables/page/use-channel-page.ts +75 -0
- package/composables/page/use-model-page.ts +67 -0
- package/composables/page/use-search-page.ts +56 -0
- package/composables/use-grid-row-after.ts +53 -0
- package/composables/use-meta.ts +60 -22
- package/composables/use-navigation-items.ts +48 -0
- package/composables/use-player-site-hooks.ts +9 -0
- package/composables/use-webcams-share.ts +14 -0
- package/package.json +1 -1
- package/pages/categories/[slug].vue +12 -85
- package/pages/channels/[channel].vue +6 -58
- package/pages/models/[slug]/index.vue +5 -44
- package/pages/playlists/[playlistId]/[slug].vue +1 -1
- package/pages/search/[slug]/index.vue +2 -50
- package/pages/videos/[slug].vue +25 -2
- package/types/index.d.ts +1 -0
- package/types/navigation-items.d.ts +28 -0
- package/types/player-site-hooks.d.ts +17 -0
- package/utils/adverts-helper.ts +31 -9
- package/utils/popunder-helper.ts +13 -3
package/DOCS/CONVENTIONS.md
CHANGED
|
@@ -16,15 +16,36 @@ namespace; filename must not repeat it.
|
|
|
16
16
|
- BEM root class = kebab-case of the name: `<CardVideo>` → `.card-video`
|
|
17
17
|
- `pathPrefix: true` mandatory — layers break component exports under `false` (nuxt/nuxt#30099)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
## Extending from a site
|
|
20
|
+
|
|
21
|
+
A site adds its own logic without copying layer code. Take the lowest rung that covers the difference; every
|
|
22
|
+
new extension point ships with a `SITE_UPDATES.md` entry, its default = current behaviour.
|
|
23
|
+
|
|
24
|
+
| Difference | Tool | Sample |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| A value: label, link, size, show/hide a block every site has | `appConfig` key (site sets it in `config/site.config.ts`) or a feature flag (env-dependent: ads, sections) | `categoriesGrouped`, `navOverrides`, `videoPage`, `gridWebcams` |
|
|
27
|
+
| "Put something here" | **Component slot**: empty `*-slot.vue` next to its owner, rendered by the owner; the site ships a file at the same path | `view/video-after-related-slot.vue` → `<ViewVideoAfterRelatedSlot :data>`, `grid/videos-end-slot.vue` |
|
|
28
|
+
| Insert **and** change inputs (props, own data) | **Wrapper**: same-name app component renders the layer one (`import Layer… from 'itube-specs/components/…'`) through its props and slots | rare: first check whether the input change is a feature every site may want — then it's a key (grids: `gridWebcams` instead of a wrapper fetching webcams) |
|
|
29
|
+
| Behaviour inside a component (ad hooks, pause screen) | **Site hooks composable**: layer calls `use*SiteHooks()` returning `{}`; the site ships the same-name composable | `usePlayerSiteHooks()` → `IPlayerSiteHooks` |
|
|
30
|
+
| Logic (which spot, how to compute) | Override the **helper** (`utils/*-helper.ts`), not the `.vue` — **extend, don't copy**: `class AdvertsHelper extends LayerAdvertsHelper` (`import { AdvertsHelper as LayerAdvertsHelper } from 'itube-specs/utils/adverts-helper'`), `static override` only what differs | `AdvertsHelper.getPrerollSpotType()`, a site's `PopunderHelper` keeps the layer's UTM part |
|
|
31
|
+
| Markup differs throughout | Override the page; its logic comes from the layer's page composables | `useCategoryPageData()` + `useCategoryPage()` |
|
|
32
|
+
| Nothing above fits | Full copy, header `copy of layer X.Y.Z, diff on update` | — target: none |
|
|
33
|
+
|
|
34
|
+
Rules that keep the rungs working:
|
|
35
|
+
|
|
36
|
+
- **Overridable helpers via auto-import only.** A layer file importing `../../utils/adverts-helper` locks out
|
|
37
|
+
the site's `utils/adverts-helper.ts` — that alone forced a fork of `player/core.vue`.
|
|
38
|
+
- **Extended helpers:** what a subclass may reuse is `protected`, not `private`; return types stay compatible
|
|
39
|
+
(`Promise<void>`). The layer module of an extended helper is imported only by its site subclass — importing it
|
|
40
|
+
first elsewhere hits the auto-import cycle (layer helper → site helper → subclass → layer helper) and `extends`
|
|
41
|
+
sees `undefined`. In tests import the site class first.
|
|
42
|
+
- **Component slots:** suffix `-slot`, file next to the component that renders it, empty in the layer. Its
|
|
43
|
+
props are a public contract — changing them is a `SITE_UPDATES.md` entry.
|
|
44
|
+
- **Wrapper:** declare only the props it reads, the rest falls through `$attrs`; forward the caller's slots
|
|
45
|
+
before adding its own. Needed hook missing → add a prop/slot here, don't fork.
|
|
46
|
+
- **Pages:** logic lives in `composables/page/use*Page.ts` — `use*PageData()` (fetches only, async, combinable
|
|
47
|
+
in `Promise.all`) + `use*Page(pageData, options)` (meta, h1, 404, breadcrumbs; sync, called after the `await`
|
|
48
|
+
so Nuxt context is intact). The page's `<script>` is the call plus page-only fetches.
|
|
28
49
|
|
|
29
50
|
## Markup
|
|
30
51
|
|
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,85 @@
|
|
|
11
11
|
коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
|
|
12
12
|
кому именно (всем / сайтам с N), что будет, если не сделать.
|
|
13
13
|
|
|
14
|
+
## 0.0.1041 — 2026-10-19
|
|
15
|
+
|
|
16
|
+
- **Сайтам со своим `composables/use-meta.ts` или форком `pages/videos/[slug].vue` ради меты (videosection-redesign):**
|
|
17
|
+
правила меты videosection.com переехали в слой. `useMeta`: для страницы > 1 сначала ищется ключ `<key>_paged` (с
|
|
18
|
+
`{p}` — номером), мета из API (`seo`) может идти через шаблоны `api_title` / `api_description` (+ `_paged`) с
|
|
19
|
+
`{text}` = значение API; нет таких ключей — всё как раньше. `useCategoryPage(pageData, { metaKeys: true })` — мета и
|
|
20
|
+
h1 категории через `useMeta` по ключам (`{text}` — название, анти-дубль слова ниши), мета категории из API — через
|
|
21
|
+
`api_*`. `useSearchPage(relatedSearch, { metaSortOptions })` — уровень сортировки в ключах меты поиска.
|
|
22
|
+
`appConfig.videoPage.metaFromPhrases` — description ролика из фраз перехода / тегов / категорий (склейка ключами
|
|
23
|
+
`meta.videos-slug.tags_and` / `tags_tail`), `{thirdText}` — название в Title Case. Свой `use-meta.ts` и форк страницы
|
|
24
|
+
видео удалить (образец — коммит videosection-redesign с этим изменением). Не сделать — копии продолжат работать,
|
|
25
|
+
но будут отставать от слоя (в форке страницы видео, например, остаётся пустой «Recent searches»). Остальным —
|
|
26
|
+
ничего, по умолчанию мета как была.
|
|
27
|
+
|
|
28
|
+
## 0.0.1040 — 2026-10-19
|
|
29
|
+
|
|
30
|
+
- **Сайтам со своими `utils/adverts-helper.ts` / `utils/popunder-helper.ts` (videosection-redesign):** не копировать
|
|
31
|
+
класс слоя, а наследовать (`class AdvertsHelper extends LayerAdvertsHelper`, импорт слойного по пути
|
|
32
|
+
`itube-specs/utils/adverts-helper`) и переопределять (`static override`) только своё. Для этого в слое: UTM-часть
|
|
33
|
+
`PopunderHelper` (`utmParamsKeys`, `saveUtmParams`) стала `protected`, добавлен `getUtmSource()`; `AdvertsHelper`
|
|
34
|
+
возвращает `Promise<void>` вместо `Promise<undefined>`, а `getAntiAdblockDomain()` делает один запрос на все
|
|
35
|
+
параллельные вызовы (раньше баннеры, попандер и слайдер на старте запрашивали домен каждый сам). Свою копию
|
|
36
|
+
`getAntiAdblockDomain` / UTM-методов удалить. Образец — коммит videosection-redesign с этим изменением. Не
|
|
37
|
+
сделать — копии продолжат работать, но будут отставать от слоя. Остальным — ничего.
|
|
38
|
+
|
|
39
|
+
- **Сайтам со своими страницами категории, поиска, канала, модели (videosection-redesign):** логика этих страниц
|
|
40
|
+
теперь в composable слоя (`composables/page/`), страницы слоя — шаблон плюс их вызов. Пара на страницу:
|
|
41
|
+
`use*PageData()` — запросы (async, можно в `Promise.all` со своими запросами страницы), `use*Page(pageData,
|
|
42
|
+
options)` — мета, h1 и 404, хлебные крошки, производные значения (синхронная, после `await`). Есть
|
|
43
|
+
`useCategoryPage*`, `useChannelPage*`, `useModelPage*` и `useSearchPage(relatedSearch)` (у поиска запросы выдачи
|
|
44
|
+
разные — только логика). Опции меты: `metaSort` — сортировка вместо `?sort`, `metaSecondText` — `{secondText}`.
|
|
45
|
+
В своей странице оставить разметку и свои блоки, скрипт — на эти composable (образец — коммит
|
|
46
|
+
videosection-redesign с этим изменением). Не сделать — ничего не сломается, но логика страницы останется копией.
|
|
47
|
+
Попутно: хлебные крошки категории, канала, модели и h1 теперь реактивны (раньше считались один раз при setup).
|
|
48
|
+
|
|
49
|
+
- **Всем сайтам:** на страницах видео и плейлиста теперь показывается блок «Recent searches» (поисковые фразы под
|
|
50
|
+
похожими). Раньше страница всегда передавала туда пустой список: проверка `typeof relatedSearch === 'object'`
|
|
51
|
+
срабатывала на самом массиве. Делать ничего не надо; если блок на сайте не нужен — сказать, заведём ключ.
|
|
52
|
+
- **Сайтам с форком `components/view/video.vue` (videosection-redesign):** состав страницы видео задаётся
|
|
53
|
+
`appConfig.videoPage` (через `config/site.config.ts`): `refererPhrases` — фразы, по которым пришли на ролик, над
|
|
54
|
+
заголовком; `relatedPagination`, `relatedModels` — пагинация похожих и карусель моделей; `webcamsBlock` — блок
|
|
55
|
+
Live Cams после похожих, `webcamInserts` — сколько моделей из хвоста его списка уходит во врезки похожих. Свои
|
|
56
|
+
блоки — в точки расширения: `components/view/video-after-player-slot.vue` (под плеером) и
|
|
57
|
+
`components/view/video-after-related-slot.vue` (после похожих и Live), в слое они пустые, сайт кладёт файлы с теми же
|
|
58
|
+
именами (компоненты-слоты — всегда с суффиксом `-slot`). После этого удалить форк `view/video.vue` (образец — коммит videosection-redesign с этим изменением).
|
|
59
|
+
Не сделать — форк продолжит работать, но будет отставать от слоя. Остальным — ничего, по умолчанию страница как
|
|
60
|
+
была.
|
|
61
|
+
|
|
62
|
+
- **Сайтам с форками `components/grid/videos.vue` / `components/grid/models.vue` (videosection-redesign):** всё
|
|
63
|
+
сайтовое в сетках — без форка и без обёртки. `appConfig.gridWebcams` (через `config/site.config.ts`): `videos: {
|
|
64
|
+
afterRows, inserts }` — блок Live Cams после N-й строки карточек и врезки вебкам-тумб (хвост того же списка) в сетке
|
|
65
|
+
видео листинга; `models: { afterRows }` — блок в основной сетке моделей. `appConfig.cardTriggerClass` — класс
|
|
66
|
+
кликабельной части карточек (например, триггер попандера). Свои вставки в конец сеток — компоненты-слоты
|
|
67
|
+
`components/grid/videos-end-slot.vue` (`items`, `related`) и `components/grid/models-end-slot.vue` (`items`,
|
|
68
|
+
`footer`), в слое пустые. Новое общее: `GridInsert` (`:after-rows`) — полноширинная вставка после N-й строки
|
|
69
|
+
карточек при любом числе колонок; `useGridRowAfter` / `placeAfterCardRows` переехали в слой — свой
|
|
70
|
+
`composables/use-grid-row-after.ts` удалить (иначе перекроет слойный); `useWebcamsShare(reserve)` — один список на
|
|
71
|
+
блок Live Cams и врезки. После этого удалить форки сеток (образец — коммит videosection-redesign с этим
|
|
72
|
+
изменением). Не сделать — форки продолжат работать, но будут отставать от слоя. Остальным — ничего, по умолчанию
|
|
73
|
+
сетки как были.
|
|
74
|
+
- **Сайтам с форком `components/player/core.vue` (videosection-redesign, hdsex-remaster):** всё сайтовое в плеере
|
|
75
|
+
теперь подключается без копии — композаблом `usePlayerSiteHooks()` (тип `IPlayerSiteHooks`): `rootClass` — класс
|
|
76
|
+
корня, `pauseScreen: { enabled, component }` — экран паузы (компонент получает `@close`, пересоздаётся на каждой
|
|
77
|
+
паузе), `onReady()` — на монтировании и на `ready` ядра. Сайт кладёт одноимённый композабл в свой `composables/`.
|
|
78
|
+
Спот преролла слой берёт из `AdvertsHelper.getPrerollSpotType()` (в слое — Primary), а сам `AdvertsHelper` и
|
|
79
|
+
`BannerHelper` — через автоимпорт, так что `utils/adverts-helper.ts` / `utils/banner-helper.ts` сайта теперь
|
|
80
|
+
доходят и до компонентов слоя. После этого удалить форк `player/core.vue` (образец — коммит videosection-redesign
|
|
81
|
+
с этим изменением). Не сделать — форк продолжит работать, но будет отставать от слоя. Сайтам со своим
|
|
82
|
+
`utils/adverts-helper.ts` без `getPrerollSpotType()` — добавить его, иначе преролл упадёт на вызове. Остальным —
|
|
83
|
+
ничего.
|
|
84
|
+
|
|
85
|
+
- **Сайтам, где меню отличается от слоя (videosection-redesign):** главная навигация (шапка и бургер) теперь из
|
|
86
|
+
композабла слоя `useNavigationItems()`, правки — в `appConfig` (через `config/site.config.ts`):
|
|
87
|
+
`navOverrides` — правка пункта слоя по его `title` (`title` — другая подпись, `link` — адрес или адреса по
|
|
88
|
+
нишам, `featureFlag`), `navExtraItems` — свои пункты (`after` — за каким пунктом слоя встать). После этого
|
|
89
|
+
удалить проектные оверрайды `components/layout/nav.vue`, `components/layout/burger.vue` и свой композабл пунктов
|
|
90
|
+
меню (образец — коммит videosection-redesign с этим изменением). Не сделать — оверрайды продолжат работать, но
|
|
91
|
+
копия `burger.vue` будет отставать от слоя. Остальным сайтам — ничего, по умолчанию меню как было.
|
|
92
|
+
|
|
14
93
|
## 0.0.1036 — 2026-10-14
|
|
15
94
|
|
|
16
95
|
- **Сайтам с `FeatureWebcamsEnabled`:** UI-ключи вебкам-блоков `webcams.*` (заголовки, счётчики онлайна, карточка,
|
package/components/adv/ntv.vue
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- Полноширинная вставка в CSS-сетку карточек (`GridVideos`, `GridModels`): встаёт после N-й строки карточек
|
|
2
|
+
при любом числе колонок (`useGridRowAfter`), пустая места не занимает. Класть прямым потомком сетки — через её слоты. -->
|
|
3
|
+
<template>
|
|
4
|
+
<div
|
|
5
|
+
ref="root"
|
|
6
|
+
class="grid-insert"
|
|
7
|
+
>
|
|
8
|
+
<slot/>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
/** После какой строки карточек встать. */
|
|
15
|
+
afterRows: number
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const root = ref<HTMLElement | null>(null);
|
|
19
|
+
|
|
20
|
+
useGridRowAfter(root, () => props.afterRows);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style lang="scss">
|
|
24
|
+
.grid-insert {
|
|
25
|
+
grid-column: 1 / -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.grid-insert:empty {
|
|
29
|
+
display: none;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- Точка расширения сетки моделей — в конце сетки, прямым потомком CSS-сетки (полноширинные вставки ставить через
|
|
2
|
+
`GridInsert` или своим `grid-column: 1 / -1`). В слое пустая; сайт кладёт свой `components/grid/models-end-slot.vue`
|
|
3
|
+
(реклама и т.п.) — он подменяет этот. -->
|
|
4
|
+
<template>
|
|
5
|
+
<template v-if="false"/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import type { IModelCard } from '../../types';
|
|
10
|
+
|
|
11
|
+
defineProps<{
|
|
12
|
+
items: IModelCard[]
|
|
13
|
+
/** Сетка в футере страницы. */
|
|
14
|
+
footer?: boolean
|
|
15
|
+
}>();
|
|
16
|
+
</script>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<!-- Сетка карточек моделей с ленивой гидрацией (первые 6 eager)
|
|
1
|
+
<!-- Сетка карточек моделей с ленивой гидрацией (первые 6 eager). Блок Live Cams — по `appConfig.gridWebcams.models`;
|
|
2
|
+
слот — полноширинные вставки (`GridInsert`); в конце — `GridModelsEndSlot` (пустой; сайт кладёт свой). -->
|
|
2
3
|
<template>
|
|
3
4
|
<div
|
|
4
5
|
:class="{'--footer': footer}"
|
|
@@ -10,19 +11,37 @@
|
|
|
10
11
|
:card="item"
|
|
11
12
|
:loading="index > 5 ? 'lazy' : 'eager'"
|
|
12
13
|
:priority="priority && index === 0"
|
|
14
|
+
:class="cardTriggerClass"
|
|
13
15
|
class="grid-models__item"
|
|
14
16
|
/>
|
|
17
|
+
<GridInsert
|
|
18
|
+
v-if="webcams"
|
|
19
|
+
:after-rows="webcams.afterRows"
|
|
20
|
+
>
|
|
21
|
+
<LazyWebcamMain hydrate-on-visible/>
|
|
22
|
+
</GridInsert>
|
|
23
|
+
<slot/>
|
|
24
|
+
<GridModelsEndSlot
|
|
25
|
+
:items="items"
|
|
26
|
+
:footer="footer"
|
|
27
|
+
/>
|
|
15
28
|
</div>
|
|
16
29
|
</template>
|
|
17
30
|
|
|
18
31
|
<script setup lang="ts">
|
|
19
32
|
import type { IModelCard } from '../../types';
|
|
20
33
|
|
|
21
|
-
defineProps<{
|
|
34
|
+
const props = defineProps<{
|
|
22
35
|
items: IModelCard[]
|
|
23
36
|
footer?: boolean
|
|
24
37
|
priority?: boolean
|
|
25
38
|
}>();
|
|
39
|
+
|
|
40
|
+
const appConfig = useAppConfig();
|
|
41
|
+
const cardTriggerClass = appConfig.cardTriggerClass || undefined;
|
|
42
|
+
|
|
43
|
+
/** Блок Live Cams — только в основной сетке страницы (`priority`), не в футере и не в сетках по буквам. */
|
|
44
|
+
const webcams = !props.footer && props.priority ? appConfig.gridWebcams?.models : null;
|
|
26
45
|
</script>
|
|
27
46
|
|
|
28
47
|
<style lang="scss">
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- Точка расширения сетки видео — в конце сетки, прямым потомком CSS-сетки (полноширинные вставки ставить через
|
|
2
|
+
`GridInsert` или своим `grid-column: 1 / -1`). В слое пустая; сайт кладёт свой `components/grid/videos-end-slot.vue`
|
|
3
|
+
(реклама в листингах и т.п.) — он подменяет этот. -->
|
|
4
|
+
<template>
|
|
5
|
+
<template v-if="false"/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import type { IVideoCard } from '../../types';
|
|
10
|
+
|
|
11
|
+
defineProps<{
|
|
12
|
+
items: IVideoCard[]
|
|
13
|
+
/** Сетка похожих на странице видео. */
|
|
14
|
+
related?: boolean
|
|
15
|
+
}>();
|
|
16
|
+
</script>
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Врезки: если передать `inserts` (вебкам-тумбы), сетка расставит их между
|
|
4
4
|
карточками — позиции считает `webcamInsertPositions` по правилам боевого движка.
|
|
5
5
|
Тумба занимает обычную ячейку сетки и попадает в тот же уровень гидрации, что и
|
|
6
|
-
карточка на её месте.
|
|
6
|
+
карточка на её месте. Без `inserts` в листинге врезки и блок Live Cams ставит сама по `appConfig.gridWebcams.videos`.
|
|
7
|
+
В конце сетки — `GridVideosEndSlot` (пустой; сайт кладёт свой: реклама и т.п.). -->
|
|
7
8
|
<template>
|
|
8
9
|
<div
|
|
9
10
|
:class="[
|
|
@@ -32,10 +33,20 @@
|
|
|
32
33
|
:top-chips="topChips"
|
|
33
34
|
:playlist-id="playlistId"
|
|
34
35
|
:playlist="playlist"
|
|
36
|
+
:link-class="cardTriggerClass"
|
|
35
37
|
class="grid-videos__card"
|
|
36
38
|
/>
|
|
37
39
|
</template>
|
|
38
40
|
<slot/>
|
|
41
|
+
<GridInsert
|
|
42
|
+
v-if="webcams"
|
|
43
|
+
:after-rows="webcams.afterRows"
|
|
44
|
+
>
|
|
45
|
+
<LazyWebcamMain
|
|
46
|
+
:reserve-tail="webcams.inserts"
|
|
47
|
+
hydrate-on-visible
|
|
48
|
+
/>
|
|
49
|
+
</GridInsert>
|
|
39
50
|
<template
|
|
40
51
|
v-for="cell in idleCells"
|
|
41
52
|
:key="`idle-${cell.key}`"
|
|
@@ -53,6 +64,7 @@
|
|
|
53
64
|
:top-chips="topChips"
|
|
54
65
|
:playlist-id="playlistId"
|
|
55
66
|
:playlist="playlist"
|
|
67
|
+
:link-class="cardTriggerClass"
|
|
56
68
|
class="grid-videos__card"
|
|
57
69
|
loading="lazy"
|
|
58
70
|
hydrate-on-idle
|
|
@@ -76,11 +88,16 @@
|
|
|
76
88
|
:playlist-id="playlistId"
|
|
77
89
|
:playlist="playlist"
|
|
78
90
|
:hydrate-on-visible="HYDRATE_MARGIN"
|
|
91
|
+
:link-class="cardTriggerClass"
|
|
79
92
|
class="grid-videos__card"
|
|
80
93
|
loading="lazy"
|
|
81
94
|
/>
|
|
82
95
|
</template>
|
|
83
96
|
<slot name="grid-end"/>
|
|
97
|
+
<GridVideosEndSlot
|
|
98
|
+
:items="items"
|
|
99
|
+
:related="related"
|
|
100
|
+
/>
|
|
84
101
|
</div>
|
|
85
102
|
</template>
|
|
86
103
|
|
|
@@ -132,15 +149,23 @@ function onInsertView(cell: IGridCell, element: HTMLElement) {
|
|
|
132
149
|
if (cell.webcam) emit('model-view', { model: cell.webcam, index: cell.insertIndex, element });
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
const appConfig = useAppConfig();
|
|
153
|
+
const cardTriggerClass = appConfig.cardTriggerClass || undefined;
|
|
154
|
+
|
|
155
|
+
/** Блок Live Cams и свои врезки — у листингов (похожие и плейлист без них, страница видео передаёт врезки сама). */
|
|
156
|
+
const webcams = !props.inserts && !props.related && !props.playlist ? appConfig.gridWebcams?.videos : null;
|
|
157
|
+
const ownInserts = webcams ? await useWebcamsShare(webcams.inserts) : null;
|
|
158
|
+
const inserts = computed(() => props.inserts || ownInserts?.value || []);
|
|
159
|
+
|
|
135
160
|
const isMobile = useState<boolean>('isMobile');
|
|
136
161
|
const eagerCount = computed(() => isMobile.value ? 4 : 12);
|
|
137
162
|
|
|
138
163
|
const cells = computed<IGridCell[]>(() => {
|
|
139
|
-
const
|
|
164
|
+
const models = inserts.value;
|
|
140
165
|
const positions = webcamInsertPositions(
|
|
141
166
|
props.insertSeed || props.items[0]?.guid || '',
|
|
142
167
|
props.items.length,
|
|
143
|
-
|
|
168
|
+
models.length,
|
|
144
169
|
);
|
|
145
170
|
|
|
146
171
|
const result: IGridCell[] = [];
|
|
@@ -148,7 +173,7 @@ const cells = computed<IGridCell[]>(() => {
|
|
|
148
173
|
|
|
149
174
|
props.items.forEach((video, index) => {
|
|
150
175
|
if (positions[inserted] === index) {
|
|
151
|
-
const model =
|
|
176
|
+
const model = models[inserted];
|
|
152
177
|
|
|
153
178
|
result.push({
|
|
154
179
|
key: `webcam-${model.id || model.username}`,
|
|
@@ -89,13 +89,11 @@ import type { IProfileData } from '../../types';
|
|
|
89
89
|
import { CSS_BREAKPOINTS } from '../../config/css-breakpoints';
|
|
90
90
|
import { AuthorizationApiService } from '../../services/api/authorization.service';
|
|
91
91
|
|
|
92
|
-
import { getNavigationItems } from '../../lib';
|
|
93
|
-
|
|
94
92
|
const { t } = useI18n();
|
|
95
93
|
const { generateLink } = useGenerateLink();
|
|
96
|
-
const navigationItems =
|
|
97
|
-
const topItems = navigationItems.filter(item => item.top);
|
|
98
|
-
const restItems = navigationItems.filter(item => !item.top);
|
|
94
|
+
const navigationItems = useNavigationItems();
|
|
95
|
+
const topItems = computed(() => navigationItems.value.filter(item => item.top));
|
|
96
|
+
const restItems = computed(() => navigationItems.value.filter(item => !item.top));
|
|
99
97
|
|
|
100
98
|
const openedChilds = ref<string | null>(null);
|
|
101
99
|
|
|
@@ -53,14 +53,13 @@
|
|
|
53
53
|
</template>
|
|
54
54
|
|
|
55
55
|
<script setup lang="ts">
|
|
56
|
-
import { getNavigationItems } from '../../lib';
|
|
57
56
|
import type { INavigationItems } from '../../types';
|
|
58
57
|
|
|
59
58
|
defineProps<{
|
|
60
59
|
items?: INavigationItems[];
|
|
61
60
|
}>();
|
|
62
61
|
|
|
63
|
-
const navigationItems =
|
|
62
|
+
const navigationItems = useNavigationItems();
|
|
64
63
|
|
|
65
64
|
const { generateLink } = useGenerateLink();
|
|
66
65
|
const route = useRoute();
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
:source="source"
|
|
10
10
|
:options="options"
|
|
11
11
|
:ad-config="loadAdConfig"
|
|
12
|
+
:class="siteHooks.rootClass"
|
|
12
13
|
class="player-core"
|
|
13
14
|
lazy
|
|
14
15
|
load-on="placeholder"
|
|
@@ -16,7 +17,20 @@
|
|
|
16
17
|
@advancerequest="onAdvanceRequest"
|
|
17
18
|
@sourcechange="onSourceChange"
|
|
18
19
|
@menurequest="onMenuRequest"
|
|
19
|
-
|
|
20
|
+
@pause="onPause"
|
|
21
|
+
@ready="siteHooks.onReady?.()"
|
|
22
|
+
>
|
|
23
|
+
<template
|
|
24
|
+
v-if="siteHooks.pauseScreen"
|
|
25
|
+
#pauseScreen="{ close }"
|
|
26
|
+
>
|
|
27
|
+
<component
|
|
28
|
+
:is="siteHooks.pauseScreen.component"
|
|
29
|
+
:key="`pause-screen-${pauseCount}`"
|
|
30
|
+
@close="close"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
33
|
+
</ITubePlayer>
|
|
20
34
|
|
|
21
35
|
<!-- Внешнее меню плеера в нашем ui-popup (только мобила — menus: 'external-mobile'). -->
|
|
22
36
|
<PlayerMenu
|
|
@@ -29,8 +43,7 @@
|
|
|
29
43
|
import { ITubePlayer } from 'itube-modern-player/vue';
|
|
30
44
|
|
|
31
45
|
import type { Player, PlayerOptions, VideoSource, LocaleCode, AdsOptions, PreviewMetaItem, SceneGroup, MenuRequestPayload } from 'itube-modern-player';
|
|
32
|
-
import {
|
|
33
|
-
import { AdSpotType, ThumbSize } from '../../runtime';
|
|
46
|
+
import { ThumbSize } from '../../runtime';
|
|
34
47
|
import type { IVideoData, IVideoCard } from '../../types';
|
|
35
48
|
import playIcon from '../../assets/icons/player/play.svg?raw';
|
|
36
49
|
import fullscreenIcon from '../../assets/icons/player/fullscreen.svg?raw';
|
|
@@ -65,6 +78,18 @@ const emit = defineEmits<{
|
|
|
65
78
|
|
|
66
79
|
const { t, n, locale } = useI18n();
|
|
67
80
|
|
|
81
|
+
const siteHooks = usePlayerSiteHooks();
|
|
82
|
+
const isPauseScreen = computed(() => !!siteHooks.pauseScreen?.enabled.value);
|
|
83
|
+
|
|
84
|
+
// Каждая пауза — новый экран: счётчик в `key` пересоздаёт его содержимое.
|
|
85
|
+
const pauseCount = ref(0);
|
|
86
|
+
|
|
87
|
+
function onPause() {
|
|
88
|
+
if (isPauseScreen.value) pauseCount.value += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onMounted(() => siteHooks.onReady?.());
|
|
92
|
+
|
|
68
93
|
const playerRef = ref<InstanceType<typeof ITubePlayer> & { player: Player | null }>();
|
|
69
94
|
|
|
70
95
|
// Внешнее меню (menus: 'external-mobile'): по нажатию меню-кнопки плеер эмитит модель,
|
|
@@ -286,7 +311,7 @@ const options = computed<Omit<PlayerOptions, 'source'>>(() => ({
|
|
|
286
311
|
playlist: {
|
|
287
312
|
externalAdvance: true,
|
|
288
313
|
},
|
|
289
|
-
pauseScreen:
|
|
314
|
+
pauseScreen: isPauseScreen.value,
|
|
290
315
|
language: playerLanguage.value,
|
|
291
316
|
// Десктоп — нативные попапы плеера; мобила (≤767px) — эмитит menurequest, рисуем сами.
|
|
292
317
|
menus: 'external-mobile',
|
|
@@ -313,7 +338,7 @@ const options = computed<Omit<PlayerOptions, 'source'>>(() => ({
|
|
|
313
338
|
// с готовым adConfig — нам не нужно синхронизировать VAST-запрос с инициализацией.
|
|
314
339
|
async function loadAdConfig(): Promise<AdsOptions | undefined> {
|
|
315
340
|
try {
|
|
316
|
-
const vastTag = await AdvertsHelper.getPrerollVastLink(
|
|
341
|
+
const vastTag = await AdvertsHelper.getPrerollVastLink(AdvertsHelper.getPrerollSpotType());
|
|
317
342
|
if (vastTag && vastTag.trim() !== '') {
|
|
318
343
|
return { adList: [{ roll: 'preRoll', vastTag }], skipDelay: 5 };
|
|
319
344
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!-- Точка расширения страницы видео — сразу под плеером. В слое пустая; сайт кладёт свой
|
|
2
|
+
`components/view/video-after-player-slot.vue` (реклама, партнёрские блоки) — он подменяет этот. -->
|
|
3
|
+
<template>
|
|
4
|
+
<template v-if="false"/>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import type { IVideoData } from '../../types';
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
data: IVideoData
|
|
12
|
+
}>();
|
|
13
|
+
</script>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!-- Точка расширения страницы видео — после похожих (и блока Live Cams, если он включён), перед поисковыми
|
|
2
|
+
фразами. В слое пустая; сайт кладёт свой `components/view/video-after-related-slot.vue` — он подменяет этот. -->
|
|
3
|
+
<template>
|
|
4
|
+
<template v-if="false"/>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import type { IVideoData } from '../../types';
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
data: IVideoData
|
|
12
|
+
}>();
|
|
13
|
+
</script>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<!-- Страница одного видео: плеер, заголовок с моделями/тегами, похожие -->
|
|
2
2
|
<template>
|
|
3
3
|
<div class="view-video _page _container">
|
|
4
|
+
<UiExpandRow
|
|
5
|
+
v-if="refererChips.length"
|
|
6
|
+
:items="refererChips"
|
|
7
|
+
class="_mb-16"
|
|
8
|
+
/>
|
|
9
|
+
|
|
4
10
|
<header class="view-video__header">
|
|
5
11
|
<h1 class="view-video__title">
|
|
6
12
|
<span class="_truncate">{{ data.title }}</span>
|
|
@@ -43,6 +49,8 @@
|
|
|
43
49
|
<slot/>
|
|
44
50
|
</PlayerMain>
|
|
45
51
|
|
|
52
|
+
<ViewVideoAfterPlayerSlot :data="data"/>
|
|
53
|
+
|
|
46
54
|
<section
|
|
47
55
|
v-if="related"
|
|
48
56
|
:class="{'_loading': status === 'pending'}"
|
|
@@ -77,7 +85,7 @@
|
|
|
77
85
|
</section>
|
|
78
86
|
|
|
79
87
|
<LazyUiPagination
|
|
80
|
-
v-if="total"
|
|
88
|
+
v-if="total && videoPage.relatedPagination"
|
|
81
89
|
arrow-icon
|
|
82
90
|
:key="`pagination-${total}`"
|
|
83
91
|
:total="total"
|
|
@@ -87,13 +95,21 @@
|
|
|
87
95
|
/>
|
|
88
96
|
|
|
89
97
|
<LazySectionSwiperCards
|
|
90
|
-
v-if="models"
|
|
98
|
+
v-if="models && videoPage.relatedModels"
|
|
91
99
|
:items="modelsRoundCards"
|
|
92
100
|
:title="$t('related_models')"
|
|
93
101
|
class="view-video__popular-models"
|
|
94
102
|
hydrate-on-visible
|
|
95
103
|
/>
|
|
96
104
|
|
|
105
|
+
<WebcamMain
|
|
106
|
+
v-if="videoPage.webcamsBlock"
|
|
107
|
+
:reserve-tail="videoPage.webcamInserts"
|
|
108
|
+
class="_mt-24"
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<ViewVideoAfterRelatedSlot :data="data"/>
|
|
112
|
+
|
|
97
113
|
<section
|
|
98
114
|
v-if="recentSearches && recentSearches.length > 0"
|
|
99
115
|
class="view-video__referer"
|
|
@@ -111,6 +127,7 @@
|
|
|
111
127
|
<script setup lang="ts">
|
|
112
128
|
import type { IChipsItem, IModelCard, IStripcashModel, IVideoCard, IVideoData, PaginatedResponse } from '../../types';
|
|
113
129
|
import { convertModelToRoundCard } from '../../runtime';
|
|
130
|
+
import { VideosApiService } from '../../services/api/videos.service';
|
|
114
131
|
import type { AsyncDataRequestStatus } from '#app';
|
|
115
132
|
|
|
116
133
|
const featureFlags = useRuntimeConfig().public.featureFlags;
|
|
@@ -136,10 +153,22 @@ const props = defineProps<{
|
|
|
136
153
|
|
|
137
154
|
const { t } = useI18n();
|
|
138
155
|
|
|
156
|
+
const videoPage = useAppConfig().videoPage;
|
|
157
|
+
|
|
139
158
|
// врезки вебкам-тумб в сетку похожих — там же, где они стоят на боевом движке.
|
|
140
|
-
//
|
|
141
|
-
// ждать провайдера.
|
|
142
|
-
|
|
159
|
+
// Без блока Live Cams запрос клиентский: сетка ниже первого экрана, и рендер страницы видео не должен
|
|
160
|
+
// ждать провайдера. С блоком список один на двоих (`useWebcamsShare`).
|
|
161
|
+
// Без флага `FeatureWebcamsEnabled` запроса нет вовсе
|
|
162
|
+
const webcamInserts = videoPage.webcamsBlock
|
|
163
|
+
? await useWebcamsShare(videoPage.webcamInserts)
|
|
164
|
+
: (await useFetchWebcams({ clientOnly: true })).items;
|
|
165
|
+
|
|
166
|
+
const { data: refererPhrases } = videoPage.refererPhrases
|
|
167
|
+
? await useFetchVideoRefererPhrases(featureFlags.FetchByUrlEnabled ? VideosApiService.refererPhrasesByUrl : VideosApiService.refererPhrasesById)
|
|
168
|
+
: { data: ref<string[] | null>(null) };
|
|
169
|
+
|
|
170
|
+
const refererChips = computed<IChipsItem[]>(() => (Array.isArray(refererPhrases.value) ? refererPhrases.value : [])
|
|
171
|
+
.map(item => ({ title: item, value: item, prefix: 'search' })));
|
|
143
172
|
|
|
144
173
|
const modelsRoundCards = computed(() =>
|
|
145
174
|
props.models?.map(convertModelToRoundCard) || []
|