itube-specs 0.0.1028 → 0.0.1031

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.1030 — 2026-10-13
15
+
16
+ - **Всем сайтам с плеером:** поднять `itube-modern-player` до `^0.15.0` (`npm install itube-modern-player@^0.15.0`).
17
+ Слой теперь отдаёт постеру ролика `fallback` — jpeg из `thumbUrls.jpeg` (его сохраняет `cleanVideoData`), и плеер
18
+ 0.15 рисует webp-постер через `<picture>` с jpeg для браузеров без webp. Не поднять — ничего не сломается, поле
19
+ молча игнорируется, фолбэка просто не будет. Сайтам с форком `components/player/core.vue` (hdsex-remaster,
20
+ videosection-redesign) — добавить `fallback` в постер, образец — коммит videosection-redesign.
21
+
22
+ ## 0.0.1029 — 2026-10-13
23
+
24
+ - **Сайтам с рекламными ссылками над контентом на мобиле (videosection-redesign):** новая вторая навигация —
25
+ `LayoutSecondNav` в `layouts/default.vue` (до 1024px, на всех страницах основного лэйаута). Пункты задаёт сайт в
26
+ `appConfig.secondNav`: `{ title, link, icon?, dot?, featureFlag? }`, `link` — строка или адреса по нишам. Пример —
27
+ `config/second-nav.config.ts` videosection-redesign. Остальным сайтам ничего не делать: без `secondNav` блока нет.
28
+
14
29
  ## 0.0.1023 — 2026-10-08
15
30
 
16
31
  - **Всем сайтам:** в `package.json` заменить скрипт `spec` на `"spec": "sh node_modules/itube-specs/scripts/spec.sh"`
@@ -1008,11 +1008,35 @@ $auth-register-link: $color-indigo-400;
1008
1008
  $auth-register-link--hover: $color-indigo-300;
1009
1009
  $auth-register-login: $color-zinc-500;
1010
1010
 
1011
+ // ─── Layout: second nav (рекламные ссылки над контентом на мобиле) ───
1012
+ $layout-second-nav-link-bg: $color-indigo-600;
1013
+ $layout-second-nav-link-text: $color-white;
1014
+ $layout-second-nav-dot-bg: $color-green-400;
1015
+
1011
1016
  // ─── Native card ───
1012
1017
  $native-card-bg: $color-black;
1013
1018
  $native-card-primary: $color-zinc-400;
1014
1019
  $native-card-secondary: $color-zinc-500;
1015
1020
 
1021
+ // ─── Adv: плашка «Реклама» поверх креатива ───
1022
+ $adv-label-text: $color-white;
1023
+ $adv-label-bg: $color-black-50;
1024
+
1025
+ // ─── Adv: партнёрская карусель FapHouse ───
1026
+ $faphouse-item-text: $video-card-text;
1027
+ $faphouse-item-text--hover: $video-card-text--hover;
1028
+ $faphouse-poster-bg: $native-card-bg;
1029
+ $faphouse-ad-text: $color-white;
1030
+ $faphouse-ad-bg: $color-indigo-600;
1031
+ $faphouse-duration-text: $color-white;
1032
+ $faphouse-duration-bg: $color-black-75;
1033
+ $faphouse-promo-bg-from: $color-amber-400;
1034
+ $faphouse-promo-bg-to: $color-amber-500;
1035
+ $faphouse-promo-text: $color-black;
1036
+ $faphouse-promo-shadow--hover: 0 10px 15px -3px #{$color-black-25};
1037
+ $faphouse-promo-action-text: $color-white;
1038
+ $faphouse-promo-action-bg: $color-red-700;
1039
+
1016
1040
  // ─── Page: grouped categories ───
1017
1041
  $grouped-highlight-border: $color-zinc-800;
1018
1042
  $grouped-category-icon: $color-zinc-500;
@@ -1035,11 +1035,35 @@ $auth-register-link: $color-indigo-soft;
1035
1035
  $auth-register-link--hover: $color-indigo-blue;
1036
1036
  $auth-register-login: $color-gray-400;
1037
1037
 
1038
+ // ─── Layout: second nav (рекламные ссылки над контентом на мобиле) ───
1039
+ $layout-second-nav-link-bg: $color-indigo-600;
1040
+ $layout-second-nav-link-text: $color-white;
1041
+ $layout-second-nav-dot-bg: $color-green-400;
1042
+
1038
1043
  // ─── Native card ───
1039
- $native-card-bg: $color-slate-800;
1040
- $native-card-primary: $color-gray-400;
1044
+ $native-card-bg: $color-white;
1045
+ $native-card-primary: $color-gray-900;
1041
1046
  $native-card-secondary: $color-gray-500;
1042
1047
 
1048
+ // ─── Adv: плашка «Реклама» поверх креатива ───
1049
+ $adv-label-text: $color-white;
1050
+ $adv-label-bg: $color-black-50;
1051
+
1052
+ // ─── Adv: партнёрская карусель FapHouse ───
1053
+ $faphouse-item-text: $video-card-text;
1054
+ $faphouse-item-text--hover: $color-indigo-600;
1055
+ $faphouse-poster-bg: $color-gray-200;
1056
+ $faphouse-ad-text: $color-white;
1057
+ $faphouse-ad-bg: $color-indigo-600;
1058
+ $faphouse-duration-text: $color-white;
1059
+ $faphouse-duration-bg: $color-black-75;
1060
+ $faphouse-promo-bg-from: $color-amber-400;
1061
+ $faphouse-promo-bg-to: $color-amber-500;
1062
+ $faphouse-promo-text: $color-black;
1063
+ $faphouse-promo-shadow--hover: 0 10px 15px -3px #{$color-black-15};
1064
+ $faphouse-promo-action-text: $color-white;
1065
+ $faphouse-promo-action-bg: $color-red-700;
1066
+
1043
1067
  // ─── Page: grouped categories ───
1044
1068
  $grouped-highlight-border: transparent;
1045
1069
  $grouped-category-icon: $color-gray-500;
@@ -0,0 +1,80 @@
1
+ <!-- Вторая навигация: ряд рекламных ссылок над контентом, только на мобиле (до бургера), на всех страницах
2
+ основного лэйаута. Пункты — `appConfig.secondNav` сайта; пусто — блока нет. -->
3
+ <template>
4
+ <nav
5
+ v-if="items.length"
6
+ class="layout-second-nav"
7
+ >
8
+ <a
9
+ v-for="item in items"
10
+ :key="item.title"
11
+ :href="item.href"
12
+ :class="{'--dot': item.dot}"
13
+ class="layout-second-nav__link"
14
+ target="_blank"
15
+ rel="nofollow noopener sponsored"
16
+ >
17
+ {{ t(`navigation.${item.title}`) }}
18
+ <UiIcon
19
+ v-if="item.icon"
20
+ :name="item.icon"
21
+ size="14"
22
+ />
23
+ </a>
24
+ </nav>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import type { ISecondNavItem } from '../../types';
29
+
30
+ const { t } = useI18n();
31
+ const { featureFlags } = useRuntimeConfig().public;
32
+ const secondNav = (useAppConfig().secondNav || []) as ISecondNavItem[];
33
+ const nicheState = useState<string>('niche');
34
+ const defaultNiche = NicheHelper.getSiteDefaultNiche();
35
+
36
+ const items = computed(() => {
37
+ const niche = nicheState.value || defaultNiche;
38
+
39
+ return secondNav
40
+ .filter(item => !item.featureFlag || featureFlags[item.featureFlag])
41
+ .map(item => ({ ...item, href: typeof item.link === 'string' ? item.link : item.link[niche] }))
42
+ .filter(item => !!item.href);
43
+ });
44
+ </script>
45
+
46
+ <style lang="scss">
47
+ .layout-second-nav {
48
+ display: flex;
49
+ justify-content: center;
50
+ gap: 12px;
51
+ padding: 8px var(--container-padding-inline);
52
+ }
53
+
54
+ .layout-second-nav__link {
55
+ @include font-sm;
56
+
57
+ position: relative;
58
+ display: inline-flex;
59
+ align-items: center;
60
+ gap: 4px;
61
+ padding: 4px 12px;
62
+ font-weight: 600;
63
+ white-space: nowrap;
64
+ color: $layout-second-nav-link-text;
65
+ background-color: $layout-second-nav-link-bg;
66
+ border-radius: $radius-md;
67
+ }
68
+
69
+ .layout-second-nav__link.--dot::after {
70
+ content: '';
71
+ position: absolute;
72
+ top: 0;
73
+ right: 0;
74
+ width: 10px;
75
+ height: 10px;
76
+ background-color: $layout-second-nav-dot-bg;
77
+ border-radius: $radius-full;
78
+ transform: translate(40%, -40%);
79
+ }
80
+ </style>
@@ -172,6 +172,8 @@ const currentSource = computed<VideoSource>(() => {
172
172
  const big = props.data.thumbUrls?.webp?.[ThumbSize.Big];
173
173
  const medium = props.data.thumbUrls?.webp?.[ThumbSize.Medium];
174
174
  const src = big || medium;
175
+ // jpeg — для браузеров без webp: плеер (0.15+) рисует <picture> с ним в <img>
176
+ const fallback = props.data.thumbUrls?.jpeg?.[ThumbSize.Big] || props.data.thumbUrls?.jpeg?.[ThumbSize.Medium];
175
177
 
176
178
  return {
177
179
  src: props.data.videoUrl,
@@ -181,6 +183,7 @@ const currentSource = computed<VideoSource>(() => {
181
183
  poster: src
182
184
  ? {
183
185
  src,
186
+ fallback,
184
187
  srcset: big && medium ? `${medium} 480w, ${big} 1280w` : undefined,
185
188
  sizes: big && medium ? '100vw' : undefined,
186
189
  width: 1280,
@@ -10,6 +10,8 @@
10
10
  hydrate-on-idle
11
11
  />
12
12
 
13
+ <LayoutSecondNav class="_to-sd"/>
14
+
13
15
  <main
14
16
  id="main"
15
17
  class="main"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1028",
4
+ "version": "0.0.1031",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -19,7 +19,7 @@ describe('cleanVideoData', () => {
19
19
  channel: { name: 'Channel', avatar_url: 'https://img.com/ch.jpg' },
20
20
  url: '/video/1',
21
21
  preview_url: 'https://img.com/preview.mp4',
22
- thumb_urls: { webp: { '320x180': 'small' } },
22
+ thumb_urls: { webp: { '320x180': 'small' }, jpeg: { '320x180': 'small-jpeg' } },
23
23
  vtt_url: '/vtt',
24
24
  vtt_sprite_url: '/sprite',
25
25
  created: 1700000000,
@@ -32,7 +32,7 @@ describe('cleanVideoData', () => {
32
32
  const result = cleanVideoData(raw as any);
33
33
  expect(result.guid).toBe('v-1');
34
34
  expect(result.channelName).toBe('Channel');
35
- expect(result.thumbUrls).toEqual({ webp: { '320x180': 'small' } });
35
+ expect(result.thumbUrls).toEqual({ webp: { '320x180': 'small' }, jpeg: { '320x180': 'small-jpeg' } });
36
36
  expect(result.vttUrl).toBe('/vtt');
37
37
  expect(result.videoUrl).toBe('/stream');
38
38
  expect(result.isDeleted).toBe(false);
@@ -150,6 +150,11 @@ describe('cleanVideoData', () => {
150
150
  ]);
151
151
  });
152
152
 
153
+ it('тумбы без jpeg — без ключа jpeg', () => {
154
+ const result = cleanVideoData({ thumb_urls: { webp: { '320x180': 'small' } } } as any);
155
+ expect(result.thumbUrls).toEqual({ webp: { '320x180': 'small' } });
156
+ });
157
+
153
158
  it('пустые данные → дефолты', () => {
154
159
  const result = cleanVideoData({} as any);
155
160
  expect(result.guid).toBe('');
@@ -84,6 +84,7 @@ export const cleanVideoData = (data: IRawVideoData, theme?: string): IVideoData
84
84
  previewUrl: data.preview_url || '',
85
85
  thumbUrls: data.thumb_urls ? {
86
86
  webp: data.thumb_urls.webp,
87
+ ...(data.thumb_urls.jpeg ? { jpeg: data.thumb_urls.jpeg } : {}),
87
88
  } : undefined,
88
89
  vttUrl: data.vtt_url || '',
89
90
  vttSpriteUrl: data.vtt_sprite_url || '',
package/types/index.d.ts CHANGED
@@ -47,6 +47,7 @@ export * from './model-filter.d.ts';
47
47
  export * from './model-filter-payload.d.ts';
48
48
  export * from './multi-suggest.d.ts';
49
49
  export * from './navigation-items.d.ts';
50
+ export * from './second-nav-item.d.ts';
50
51
  export * from './link-item.d.ts';
51
52
  export * from './fluid-player.d.ts';
52
53
  export * from './chips-item.d.ts';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Пункт второй навигации — ряд рекламных ссылок над контентом на мобиле (`appConfig.secondNav`).
3
+ * `link` — один адрес или адреса по нишам (`{ straight, gay, shemale }`); нет адреса для ниши — пункт
4
+ * не показывается. `title` — ключ `navigation.<title>`.
5
+ */
6
+ export interface ISecondNavItem {
7
+ title: string
8
+ link: string | Record<string, string>
9
+ icon?: string
10
+ /** Зелёная точка «онлайн» в углу пункта. */
11
+ dot?: boolean
12
+ /** Имя фича-флага сайта: флаг выключен — пункта нет. */
13
+ featureFlag?: string
14
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ThumbSize } from '../runtime/constants/thumb-size';
2
2
 
3
- /** Очищенные тумбы — только webp */
3
+ /** Очищенные тумбы: webp; jpeg — только у ролика целиком, фолбэк постера плеера. */
4
4
  export interface IThumbUrls {
5
5
  webp: Partial<Record<ThumbSize, string>>;
6
+ jpeg?: Partial<Record<ThumbSize, string>>;
6
7
  }