itube-specs 0.0.1058 → 0.0.1059

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/SITE_UPDATES.md CHANGED
@@ -11,6 +11,21 @@
11
11
  коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
12
12
  кому именно (всем / сайтам с N), что будет, если не сделать.
13
13
 
14
+ ## 0.0.1059 — 2026-10-30
15
+
16
+ - **Всем, срочно к сведению:** с 0.0.1056 блок Live Cams и врезки вебкамов пропали из сеток видео на страницах, которые
17
+ не передают `webcamsQuery` (канал, поиск, листинги на `ViewVideos` без запроса): проп был `{…} | false`, и Vue приводил
18
+ отсутствующий булев проп к `false` = «без вебкамов». Починено. Сайтам, где сетке передавали `:webcams-query="false"`,
19
+ — заменить на `no-webcams` (новый булев проп `GridVideos` / `ViewVideos`), иначе вебкамы там снова появятся.
20
+
21
+ - **Всем — к сведению:** `GridInsert` / `useGridRowAfter` умеют `afterCards` — после строки с N-й карточкой (без дыр,
22
+ вебкам-тумбы с `data-grid-extra` не в счёт; карточек меньше — за последней строкой); пересчёт и на перестановку
23
+ соседних вставок (раньше реклама могла сдвинуть блок, а он не замечал). Новые ключи: `gridWebcams.videos.afterCards`,
24
+ `gridWebcams.models.afterCards` (важнее `afterRows`), `videoPage.webcamsInRelatedAfter` — блок Live Cams внутри
25
+ сетки похожих. Дефолты — как было, делать ничего не надо; videosection-redesign ставит 14 / 10 / 14.
26
+ - **Всем — к сведению:** у `WebcamMain` проп `fullRows` (по умолчанию `true` — только полные строки, как было);
27
+ `false` — все модели, последняя строка может быть неполной.
28
+
14
29
  ## 0.0.1058 — 2026-10-30
15
30
 
16
31
  - **Всем — к сведению:** новый ключ `appConfig.channelTagsSource` (по умолчанию `'popular'` — как было, популярные теги
package/app.config.ts CHANGED
@@ -61,8 +61,9 @@ export default defineAppConfig({
61
61
  * карточек и сколько врезок вебкам-тумб (хвост того же списка) в сетке видео листинга (не похожие, не плейлист,
62
62
  * не когда страница передала врезки сама); `models` — в основной сетке моделей (`priority`, не футер). */
63
63
  gridWebcams: {
64
- videos: null as { afterRows: number, inserts: number } | null,
65
- models: null as { afterRows: number } | null,
64
+ /** `afterCards` — после строки с N-й карточкой (карточек меньше — в конец сетки); задан — важнее `afterRows`. */
65
+ videos: null as { afterRows: number, afterCards?: number, inserts: number } | null,
66
+ models: null as { afterRows: number, afterCards?: number } | null,
66
67
  /** Раскладка videosection.com (`WEBCAMS_LEGACY_LAYOUT`): список 42 на страницу, блок — модели 0–11, врезки в сетку
67
68
  * видео — 21–24 после ¼, ½, ¾ сетки и за 4 карточки до конца (`inserts` из `videos` не используется), мини-блок
68
69
  * после сетки видео и моделей — 25–36. `false` — раскладка слоя: блок 12 + врезки из хвоста на случайных местах. */
@@ -86,6 +87,9 @@ export default defineAppConfig({
86
87
  relatedModels: true,
87
88
  /** Блок Live Cams после похожих (при `FeatureWebcamsEnabled`); врезки в похожие берут хвост того же списка. */
88
89
  webcamsBlock: false,
90
+ /** Блок Live Cams внутри сетки похожих — после строки с N-й карточкой (похожих меньше — в конце сетки);
91
+ * `null` — после секции похожих. */
92
+ webcamsInRelatedAfter: null as number | null,
89
93
  /** Сколько моделей из хвоста списка уходит во врезки похожих, когда блок Live Cams включён. */
90
94
  webcamInserts: 8,
91
95
  /** С 1024px прятать чипсы моделей над плеером, у которых есть блок в боковой колонке (не дублировать ссылку). */
@@ -1,5 +1,6 @@
1
1
  <!-- Полноширинная вставка в CSS-сетку карточек (`GridVideos`, `GridModels`): встаёт после N-й строки карточек
2
- при любом числе колонок (`useGridRowAfter`), пустая места не занимает. Класть прямым потомком сетки — через её слоты. -->
2
+ (`afterRows`) или после строки с N-й карточкой (`afterCards`, карточек меньше — в конец) при любом числе колонок
3
+ (`useGridRowAfter`), пустая места не занимает. Класть прямым потомком сетки — через её слоты. -->
3
4
  <template>
4
5
  <div
5
6
  ref="root"
@@ -12,12 +13,14 @@
12
13
  <script setup lang="ts">
13
14
  const props = defineProps<{
14
15
  /** После какой строки карточек встать. */
15
- afterRows: number
16
+ afterRows?: number
17
+ /** После строки, в которой лежит N-я карточка (без дыр перед блоком); задан — важнее `afterRows`. */
18
+ afterCards?: number
16
19
  }>();
17
20
 
18
21
  const root = ref<HTMLElement | null>(null);
19
22
 
20
- useGridRowAfter(root, () => props.afterRows);
23
+ useGridRowAfter(root, () => (props.afterCards ? { cards: props.afterCards } : { rows: props.afterRows ?? 0 }));
21
24
  </script>
22
25
 
23
26
  <style lang="scss">
@@ -17,6 +17,7 @@
17
17
  <GridInsert
18
18
  v-if="webcams"
19
19
  :after-rows="webcams.afterRows"
20
+ :after-cards="webcams.afterCards"
20
21
  >
21
22
  <LazyWebcamMain
22
23
  :list="legacy ? WEBCAMS_LEGACY_LAYOUT.list : undefined"
@@ -21,6 +21,7 @@
21
21
  v-if="cell.webcam"
22
22
  :model="cell.webcam"
23
23
  class="grid-videos__webcam"
24
+ data-grid-extra
24
25
  @click="onInsertClick(cell)"
25
26
  @poster="element => onInsertView(cell, element)"
26
27
  />
@@ -44,6 +45,7 @@
44
45
  <GridInsert
45
46
  v-if="webcams"
46
47
  :after-rows="webcams.afterRows"
48
+ :after-cards="webcams.afterCards"
47
49
  >
48
50
  <LazyWebcamMain
49
51
  v-if="legacyList"
@@ -64,6 +66,7 @@
64
66
  v-if="cell.webcam"
65
67
  :model="cell.webcam"
66
68
  class="grid-videos__webcam"
69
+ data-grid-extra
67
70
  @click="onInsertClick(cell)"
68
71
  @poster="element => onInsertView(cell, element)"
69
72
  />
@@ -90,6 +93,7 @@
90
93
  v-if="cell.webcam"
91
94
  :model="cell.webcam"
92
95
  class="grid-videos__webcam"
96
+ data-grid-extra
93
97
  @click="onInsertClick(cell)"
94
98
  @poster="element => onInsertView(cell, element)"
95
99
  />
@@ -159,9 +163,11 @@ const props = defineProps<{
159
163
  insertSeed?: string
160
164
  /**
161
165
  * Выборка вебкамов листинга при `gridWebcams.legacy`: категория провайдера, `random` и размер списка (по умолчанию
162
- * `WEBCAMS_LEGACY_LAYOUT.list`, случайные). `false` — без блока и врезок (в любой раскладке).
166
+ * `WEBCAMS_LEGACY_LAYOUT.list`, случайные). Только объект: булев тип в пропе Vue приводит отсутствующий проп к `false`.
163
167
  */
164
- webcamsQuery?: { category?: string, random?: boolean, list?: number } | false
168
+ webcamsQuery?: { category?: string, random?: boolean, list?: number }
169
+ /** Без блока Live Cams и врезок (в любой раскладке). */
170
+ noWebcams?: boolean
165
171
  }>();
166
172
 
167
173
  /** Клики и показы врезанных тумб уходят наружу — статистику отправляет сайт. */
@@ -189,16 +195,16 @@ const cardTriggerClass = appConfig.cardTriggerClass || undefined;
189
195
  const legacy = !!appConfig.gridWebcams?.legacy;
190
196
 
191
197
  /** Блок Live Cams и свои врезки — у листингов (похожие и плейлист без них, страница видео передаёт врезки сама). */
192
- const webcams = !props.inserts && !props.related && !props.playlist && props.webcamsQuery !== false
198
+ const webcams = !props.inserts && !props.related && !props.playlist && !props.noWebcams
193
199
  ? appConfig.gridWebcams?.videos
194
200
  : null;
195
201
 
196
202
  /** Общий список страницы в раскладке videosection.com (`gridWebcams.legacy`): блок, врезки и мини-блок режут его по индексам. */
197
203
  const legacyList = webcams && legacy
198
204
  ? {
199
- list: (props.webcamsQuery && props.webcamsQuery.list) || WEBCAMS_LEGACY_LAYOUT.list,
200
- category: (props.webcamsQuery && props.webcamsQuery.category) || undefined,
201
- random: props.webcamsQuery ? props.webcamsQuery.random !== false : true,
205
+ list: props.webcamsQuery?.list || WEBCAMS_LEGACY_LAYOUT.list,
206
+ category: props.webcamsQuery?.category || undefined,
207
+ random: props.webcamsQuery?.random !== false,
202
208
  }
203
209
  : null;
204
210
 
@@ -81,6 +81,20 @@
81
81
  <template #grid-end>
82
82
  <AdvNativeBottom v-if="featureFlags.AdsNativeEnabled"/>
83
83
  </template>
84
+
85
+ <GridInsert
86
+ v-if="videoPage.webcamsBlock && videoPage.webcamsInRelatedAfter"
87
+ :after-cards="videoPage.webcamsInRelatedAfter"
88
+ >
89
+ <WebcamMain
90
+ v-if="legacyWebcams"
91
+ v-bind="legacyWebcams"
92
+ />
93
+ <WebcamMain
94
+ v-else
95
+ :reserve-tail="videoPage.webcamInserts"
96
+ />
97
+ </GridInsert>
84
98
  </LazyGridVideos>
85
99
  </section>
86
100
 
@@ -102,16 +116,18 @@
102
116
  hydrate-on-visible
103
117
  />
104
118
 
105
- <WebcamMain
106
- v-if="videoPage.webcamsBlock && legacyWebcams"
107
- v-bind="legacyWebcams"
108
- class="_mt-24"
109
- />
110
- <WebcamMain
111
- v-else-if="videoPage.webcamsBlock"
112
- :reserve-tail="videoPage.webcamInserts"
113
- class="_mt-24"
114
- />
119
+ <template v-if="videoPage.webcamsBlock && !videoPage.webcamsInRelatedAfter">
120
+ <WebcamMain
121
+ v-if="legacyWebcams"
122
+ v-bind="legacyWebcams"
123
+ class="_mt-24"
124
+ />
125
+ <WebcamMain
126
+ v-else
127
+ :reserve-tail="videoPage.webcamInserts"
128
+ class="_mt-24"
129
+ />
130
+ </template>
115
131
 
116
132
  <ViewVideoAfterRelatedSlot :data="data"/>
117
133
 
@@ -31,6 +31,7 @@
31
31
  v-if="data && data.items && data.items?.length > 0"
32
32
  :items="data.items"
33
33
  :webcams-query="webcamsQuery"
34
+ :no-webcams="noWebcams"
34
35
  :class="{'_loading': status === 'pending'}"
35
36
  class="view-videos__grid"
36
37
  id="anchor"
@@ -118,7 +119,9 @@ const props = defineProps<{
118
119
  /** Чипсы поисковых фраз во второй строке грида, как на странице категории. */
119
120
  relatedSearch?: IChipsItem[]
120
121
  /** Выборка вебкамов сетки (`GridVideos`). */
121
- webcamsQuery?: { category?: string, random?: boolean, list?: number } | false
122
+ webcamsQuery?: { category?: string, random?: boolean, list?: number }
123
+ /** Сетка без вебкамов (`GridVideos`). */
124
+ noWebcams?: boolean
122
125
  }>();
123
126
 
124
127
  const route = useRoute();
@@ -55,10 +55,13 @@ const props = withDefaults(defineProps<{
55
55
  id?: string
56
56
  /** Без счётчиков онлайна в шапке (мини-блок videosection.com). */
57
57
  hideCounts?: boolean
58
+ /** Только полные строки (по умолчанию); `false` — все модели, последняя строка может быть неполной. */
59
+ fullRows?: boolean
58
60
  }>(), {
59
61
  reserveTail: 0,
60
62
  offset: 0,
61
63
  random: true,
64
+ fullRows: true,
62
65
  });
63
66
 
64
67
  const NO_COUNTS = { total: 0, byLanguages: {} };
@@ -103,7 +106,7 @@ const columns = ref(0);
103
106
  */
104
107
  const visibleItems = computed<IStripcashModel[]>(() => {
105
108
  const cols = columns.value;
106
- if (!cols) return blockItems.value;
109
+ if (!cols || !props.fullRows) return blockItems.value;
107
110
 
108
111
  return blockItems.value.slice(0, Math.floor(blockItems.value.length / cols) * cols);
109
112
  });
@@ -1,35 +1,61 @@
1
1
  import { useDebounceFn } from '@vueuse/core';
2
2
 
3
+ /** Где встать: после N строк карточек или после строки, в которой лежит N-я карточка. */
4
+ export type GridInsertPlace = { rows: number } | { cards: number };
5
+
3
6
  /**
4
7
  * Ставит полноширинный блок CSS-сетки после N-й строки карточек — при любом числе колонок.
5
8
  *
6
9
  * Считаем по карточкам, а не по строкам сетки: в сетку попадают и полноширинные врезки (карусель
7
10
  * моделей, блок вебкамов, реклама), они занимают строку целиком. Карточку отличаем геометрически —
8
- * шириной в одну колонку. Карточек до нужной строки не хватает — блок остаётся на своём месте в DOM.
11
+ * шириной в одну колонку; скрытые (`display: none`) не считаем.
12
+ *
13
+ * `{ cards: N }` — после строки с N-й карточкой (без дыр в строке перед блоком; врезки с `data-grid-extra` не в счёт);
14
+ * карточек меньше — сразу за строкой последней. `{ rows: N }` — карточек до нужной строки не хватает — блок остаётся
15
+ * на своём месте в DOM.
9
16
  */
10
- export function placeAfterCardRows(el: HTMLElement | null, rows: number) {
17
+ export function placeAfterCardRows(el: HTMLElement | null, place: number | GridInsertPlace) {
11
18
  const grid = el?.parentElement;
12
19
  if (!el || !grid) return;
13
20
 
14
21
  const columns = getComputedStyle(grid).gridTemplateColumns.split(' ').filter(Boolean);
15
22
  const columnWidth = Number.parseFloat(columns[0]) || 0;
16
23
 
17
- const siblings = Array.from(grid.children).filter(item => item !== el) as HTMLElement[];
24
+ const siblings = (Array.from(grid.children) as HTMLElement[]).filter(item => item !== el && item.offsetWidth > 0);
18
25
  const cards = siblings.filter(item => item.offsetWidth <= columnWidth * 1.5);
26
+ const setRow = (row: string) => {
27
+ if (el.style.gridRow !== row) el.style.gridRow = row;
28
+ };
29
+
30
+ if (typeof place === 'object' && 'cards' in place) {
31
+ // N-я карточка выдачи (врезки с `data-grid-extra` — вебкам-тумбы — не в счёт) или последняя, если их меньше;
32
+ // блок — сразу за её строкой
33
+ const counted = cards.filter(item => !item.hasAttribute('data-grid-extra'));
34
+ const anchor = counted[place.cards - 1] || counted.at(-1);
35
+ const rowsUpToAnchor = anchor
36
+ ? new Set(siblings.filter(item => item.offsetTop <= anchor.offsetTop).map(item => item.offsetTop)).size
37
+ : 0;
38
+
39
+ setRow(anchor ? String(rowsUpToAnchor + 1) : '');
40
+
41
+ return;
42
+ }
43
+
44
+ const target = cards[(typeof place === 'number' ? place : place.rows) * columns.length];
19
45
 
20
- const target = cards[rows * columns.length];
21
46
  if (!target) {
22
- el.style.gridRow = '';
47
+ setRow('');
23
48
 
24
49
  return;
25
50
  }
26
51
 
27
52
  const rowsAbove = new Set(siblings.filter(item => item.offsetTop < target.offsetTop).map(item => item.offsetTop));
28
- el.style.gridRow = String(rowsAbove.size + 1);
53
+ setRow(String(rowsAbove.size + 1));
29
54
  }
30
55
 
31
- /** То же для компонента: пересчёт на resize (меняются колонки) и на доезд карточек (ленивая гидрация). */
32
- export const useGridRowAfter = (el: Ref<HTMLElement | null>, rows: () => number) => {
56
+ /** То же для компонента: пересчёт на resize (меняются колонки), на доезд карточек (ленивая гидрация) и на перестановку
57
+ * соседних вставок. */
58
+ export const useGridRowAfter = (el: Ref<HTMLElement | null>, rows: () => number | GridInsertPlace) => {
33
59
  const place = () => placeAfterCardRows(el.value, rows());
34
60
 
35
61
  onMounted(() => {
@@ -37,11 +63,19 @@ export const useGridRowAfter = (el: Ref<HTMLElement | null>, rows: () => number)
37
63
  if (!grid) return;
38
64
 
39
65
  const replace = useDebounceFn(place, 200);
40
- const observer = new MutationObserver(replace);
66
+ // соседние вставки (реклама, другие блоки) тоже переставляют себя через style — пересчитываемся вслед за ними;
67
+ // style внутри карточек (превью и пр.) не интересен — только у прямых детей сетки, кроме себя
68
+ const observer = new MutationObserver((mutations) => {
69
+ if (mutations.some(item => (item.type === 'childList'
70
+ ? item.target === grid
71
+ : item.target.parentElement === grid && item.target !== el.value))) {
72
+ replace();
73
+ }
74
+ });
41
75
 
42
76
  place();
43
77
  window.addEventListener('resize', replace, { passive: true });
44
- observer.observe(grid, { childList: true });
78
+ observer.observe(grid, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
45
79
 
46
80
  onBeforeUnmount(() => {
47
81
  window.removeEventListener('resize', replace);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1058",
4
+ "version": "0.0.1059",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {