itube-specs 0.0.1057 → 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 +29 -0
- package/app.config.ts +9 -2
- package/components/grid/insert.vue +6 -3
- package/components/grid/models.vue +1 -0
- package/components/grid/videos.vue +12 -6
- package/components/view/video.vue +26 -10
- package/components/view/videos.vue +4 -1
- package/components/webcam/main.vue +4 -1
- package/composables/fetch/use-search-related-phrases.ts +13 -7
- package/composables/page/use-channel-page.ts +13 -5
- package/composables/use-grid-row-after.ts +44 -10
- package/package.json +1 -1
- package/services/api/playlists.service.ts +4 -2
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,35 @@
|
|
|
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
|
+
|
|
29
|
+
## 0.0.1058 — 2026-10-30
|
|
30
|
+
|
|
31
|
+
- **Всем — к сведению:** новый ключ `appConfig.channelTagsSource` (по умолчанию `'popular'` — как было, популярные теги
|
|
32
|
+
роликов канала). `'channel'` — теги самого канала из его карточки, без тегов блока нет, запрос популярных тегов не
|
|
33
|
+
идёт. Делать ничего не надо; videosection-redesign ставит `'channel'`.
|
|
34
|
+
|
|
35
|
+
- **Всем, срочно к сведению:** в 0.0.1056 страница поиска (`/search/{q}`, страница слоя и сайтовые на
|
|
36
|
+
`useSearchRelatedPhrases()`) падала с 500 — композабл звал `useAppConfig` после `await`, вне контекста Nuxt. Починено,
|
|
37
|
+
делать ничего не надо — обновиться.
|
|
38
|
+
|
|
39
|
+
- **Всем — к сведению:** ролики плейлиста приходят целиком (до 200) — `getVideosByPlaylist` слал `limit=100`, которого
|
|
40
|
+
ручка не знает, и получал только первые 20: на плеере плейлиста список, «следующий» и перемешивание ходили по 20,
|
|
41
|
+
счётчик показывал `N/20`. Теперь `per-page` (по умолчанию 200, как на плеере videosection.com). Делать ничего не надо.
|
|
42
|
+
|
|
14
43
|
## 0.0.1056 — 2026-10-30
|
|
15
44
|
|
|
16
45
|
- **Всем — к сведению:** починена `/search/{q}/models` — метод получал аргументы не в том порядке (фраза уходила в
|
package/app.config.ts
CHANGED
|
@@ -17,6 +17,9 @@ export default defineAppConfig({
|
|
|
17
17
|
/** Есть ли у сайта сгруппированный вид категорий (`/categories` — группы, `/categories/all` — A–Z).
|
|
18
18
|
* `false` — `/categories` сам список A–Z: `CategoryNav` без табов Grouped / A-Z, «All» ведёт на `/categories`. */
|
|
19
19
|
categoriesGrouped: true,
|
|
20
|
+
/** Чипсы тегов на странице канала: `popular` — популярные теги роликов канала (`popular-tags-by-channel-name`),
|
|
21
|
+
* `channel` — теги самого канала из его карточки (`tags`), нет тегов — нет блока (как на videosection.com). */
|
|
22
|
+
channelTagsSource: 'popular' as 'popular' | 'channel',
|
|
20
23
|
/** Порядок выдачи без `?sort` (значение `order` API). `null` — дефолт слоя, `''` — `order` не передавать (дефолт
|
|
21
24
|
* API). Вкладки сортировок не меняет. */
|
|
22
25
|
defaultOrders: {
|
|
@@ -58,8 +61,9 @@ export default defineAppConfig({
|
|
|
58
61
|
* карточек и сколько врезок вебкам-тумб (хвост того же списка) в сетке видео листинга (не похожие, не плейлист,
|
|
59
62
|
* не когда страница передала врезки сама); `models` — в основной сетке моделей (`priority`, не футер). */
|
|
60
63
|
gridWebcams: {
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
/** `afterCards` — после строки с N-й карточкой (карточек меньше — в конец сетки); задан — важнее `afterRows`. */
|
|
65
|
+
videos: null as { afterRows: number, afterCards?: number, inserts: number } | null,
|
|
66
|
+
models: null as { afterRows: number, afterCards?: number } | null,
|
|
63
67
|
/** Раскладка videosection.com (`WEBCAMS_LEGACY_LAYOUT`): список 42 на страницу, блок — модели 0–11, врезки в сетку
|
|
64
68
|
* видео — 21–24 после ¼, ½, ¾ сетки и за 4 карточки до конца (`inserts` из `videos` не используется), мини-блок
|
|
65
69
|
* после сетки видео и моделей — 25–36. `false` — раскладка слоя: блок 12 + врезки из хвоста на случайных местах. */
|
|
@@ -83,6 +87,9 @@ export default defineAppConfig({
|
|
|
83
87
|
relatedModels: true,
|
|
84
88
|
/** Блок Live Cams после похожих (при `FeatureWebcamsEnabled`); врезки в похожие берут хвост того же списка. */
|
|
85
89
|
webcamsBlock: false,
|
|
90
|
+
/** Блок Live Cams внутри сетки похожих — после строки с N-й карточкой (похожих меньше — в конце сетки);
|
|
91
|
+
* `null` — после секции похожих. */
|
|
92
|
+
webcamsInRelatedAfter: null as number | null,
|
|
86
93
|
/** Сколько моделей из хвоста списка уходит во врезки похожих, когда блок Live Cams включён. */
|
|
87
94
|
webcamInserts: 8,
|
|
88
95
|
/** С 1024px прятать чипсы моделей над плеером, у которых есть блок в боковой колонке (не дублировать ссылку). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!-- Полноширинная вставка в CSS-сетку карточек (`GridVideos`, `GridModels`): встаёт после N-й строки карточек
|
|
2
|
-
|
|
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
|
|
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">
|
|
@@ -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`, случайные).
|
|
166
|
+
* `WEBCAMS_LEGACY_LAYOUT.list`, случайные). Только объект: булев тип в пропе Vue приводит отсутствующий проп к `false`.
|
|
163
167
|
*/
|
|
164
|
-
webcamsQuery?: { category?: string, random?: boolean, list?: number }
|
|
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.
|
|
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:
|
|
200
|
-
category:
|
|
201
|
-
random: props.webcamsQuery
|
|
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
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 }
|
|
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,22 +1,28 @@
|
|
|
1
|
-
import type { ComputedRef } from 'vue';
|
|
1
|
+
import type { ComputedRef, Ref } from 'vue';
|
|
2
2
|
import { SearchApiService } from '../../services/api/search.service';
|
|
3
|
+
import type { IRelatedPhrases } from '../../types';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Фразы для чипсов страницы поиска (`useSearchPage`), без самой фразы поиска. С `appConfig.relatedSearches.queryLimit`
|
|
6
7
|
* — топ похожих на запрос (`/video/related-searches`), без него — общие фразы (`/search/phrases`). Порядок —
|
|
7
|
-
* `useRelatedPhrases`.
|
|
8
|
+
* `useRelatedPhrases`. Всё, что требует контекста Nuxt, вызывается до `await`: после него в обычной функции контекста нет.
|
|
8
9
|
*/
|
|
9
10
|
export async function useSearchRelatedPhrases(): Promise<ComputedRef<string[]>> {
|
|
10
11
|
const { queryLimit } = useAppConfig().relatedSearches;
|
|
11
12
|
const query = useSlug();
|
|
13
|
+
const source = shallowRef<Ref<IRelatedPhrases | string[] | null | undefined> | null>(null);
|
|
14
|
+
|
|
15
|
+
const phrases = useRelatedPhrases(() => {
|
|
16
|
+
const value = source.value?.value;
|
|
17
|
+
const items = Array.isArray(value) ? value : value?.items;
|
|
18
|
+
return items?.filter(item => (typeof item === 'string' ? item : item.query) !== query.value);
|
|
19
|
+
}, useRelatedSearchLimit('queryLimit'));
|
|
12
20
|
|
|
13
21
|
const { data } = queryLimit === null
|
|
14
22
|
? await useFetchRelatedSearch(SearchApiService.getPhrases)
|
|
15
23
|
: await useFetchRelatedSearchesByQuery(SearchApiService.getRelatedSearches, query);
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return items?.filter(item => (typeof item === 'string' ? item : item.query) !== query.value);
|
|
21
|
-
}, useRelatedSearchLimit('queryLimit'));
|
|
25
|
+
source.value = data;
|
|
26
|
+
|
|
27
|
+
return phrases;
|
|
22
28
|
}
|
|
@@ -7,13 +7,14 @@ import { VideosApiService } from '../../services/api/videos.service';
|
|
|
7
7
|
import type { IBreadcrumbItem, IChipsItem, IPopularTags, INavLinkItem } from '../../types';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Данные страницы канала: карточка канала, видео, популярные
|
|
10
|
+
* Данные страницы канала: карточка канала, видео, популярные теги (при `appConfig.channelTagsSource: 'popular'`). Только запросы — их можно запускать в
|
|
11
11
|
* `Promise.all` вместе с запросами одной страницы (модели канала). Каналы сети зависят от карточки, их страница
|
|
12
12
|
* запрашивает после. Разметку страница держит свою, логику — `useChannelPage()`.
|
|
13
13
|
*/
|
|
14
14
|
export async function useChannelPageData() {
|
|
15
15
|
const lang = useLang() as Language;
|
|
16
16
|
const redirectLegacySlug = useLegacySlugRedirect('channel', name => ChannelsApiService.getChannelsByName(lang, name).then(Boolean));
|
|
17
|
+
const popularTags = useAppConfig().channelTagsSource !== 'channel';
|
|
17
18
|
|
|
18
19
|
const [
|
|
19
20
|
{ data: cardInfo, status },
|
|
@@ -22,7 +23,9 @@ export async function useChannelPageData() {
|
|
|
22
23
|
] = await Promise.all([
|
|
23
24
|
useFetchChannelsByName(ChannelsApiService.getChannelsByName),
|
|
24
25
|
useFetchVideosByChannel(VideosApiService.getListByNicheAndChannel),
|
|
25
|
-
|
|
26
|
+
popularTags
|
|
27
|
+
? useFetchPopularTagsByChannelName(VideosApiService.popularTagsByChannelName)
|
|
28
|
+
: { data: ref<IPopularTags[] | null>(null) },
|
|
26
29
|
]);
|
|
27
30
|
|
|
28
31
|
if (!cardInfo.value) await redirectLegacySlug();
|
|
@@ -76,9 +79,14 @@ export function useChannelPage(
|
|
|
76
79
|
{ name: title.value },
|
|
77
80
|
]);
|
|
78
81
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const fromChannel = useAppConfig().channelTagsSource === 'channel';
|
|
83
|
+
|
|
84
|
+
const channelTags = computed<IChipsItem[]>(() => (fromChannel
|
|
85
|
+
? (cardInfo.value?.tags || [])
|
|
86
|
+
: (channelTagsData.value || []).map((item: IPopularTags) => item.title)
|
|
87
|
+
).map(tag => ({
|
|
88
|
+
title: tag,
|
|
89
|
+
value: tag,
|
|
82
90
|
prefix: 'tag',
|
|
83
91
|
})));
|
|
84
92
|
|
|
@@ -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
|
-
* шириной в одну
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
setRow(String(rowsAbove.size + 1));
|
|
29
54
|
}
|
|
30
55
|
|
|
31
|
-
/** То же для компонента: пересчёт на resize (меняются колонки)
|
|
32
|
-
|
|
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
|
-
|
|
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
|
@@ -78,11 +78,13 @@ export class PlaylistsApiService {
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Ролики плейлиста. Размер страницы — `per-page` (`limit` ручка не знает); без него в `pagination` — 200, весь
|
|
82
|
+
* плейлист одной страницей, как на плеере плейлиста videosection.com. */
|
|
81
83
|
static async getVideosByPlaylist(
|
|
82
84
|
id: string,
|
|
83
85
|
pagination: RequestPagination,
|
|
84
86
|
lang: Language,
|
|
85
|
-
|
|
87
|
+
perPage = 200,
|
|
86
88
|
): Promise<PaginatedResponse<IVideoCard>> {
|
|
87
89
|
return await ApiHelper.fetch(
|
|
88
90
|
`/playlist/get-playlist-videos-by-id/${id}`,
|
|
@@ -94,7 +96,7 @@ export class PlaylistsApiService {
|
|
|
94
96
|
'Content-Type': 'application/json',
|
|
95
97
|
},
|
|
96
98
|
params: {
|
|
97
|
-
|
|
99
|
+
'per-page': perPage,
|
|
98
100
|
...pagination,
|
|
99
101
|
},
|
|
100
102
|
},
|