itube-specs 0.0.1034 → 0.0.1037

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.
@@ -19,6 +19,13 @@ namespace; filename must not repeat it.
19
19
  **App override:** an app defining the same name outranks the layer and wins auto-import; callers untouched.
20
20
  Don't fork a component when a prop expresses the difference.
21
21
 
22
+ **Small difference → flag or prop in the layer, not an override.** One or two tweaks (hide a block, another
23
+ link, another spacing), and anything another site may want too: add a prop, an `appConfig` key (default =
24
+ current behaviour, the site sets it in its `config/site.config.ts`) or a feature flag here, plus a
25
+ `SITE_UPDATES.md` entry. No copied page or component in an app slot for that — a fork goes stale with every
26
+ layer release. Override only when the app's version genuinely differs throughout (e.g. a site's ad-aware
27
+ `grid/videos.vue`). Sample: `appConfig.categoriesGrouped` instead of an app copy of `category/nav.vue`.
28
+
22
29
  ## Markup
23
30
 
24
31
  `<div>` is the fallback, not the default.
package/SITE_UPDATES.md CHANGED
@@ -11,6 +11,20 @@
11
11
  коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
12
12
  кому именно (всем / сайтам с N), что будет, если не сделать.
13
13
 
14
+ ## 0.0.1036 — 2026-10-14
15
+
16
+ - **Сайтам с `FeatureWebcamsEnabled`:** UI-ключи вебкам-блоков `webcams.*` (заголовки, счётчики онлайна, карточка,
17
+ фильтры, переключатель разделов) теперь в локалях слоя — перенесены из videosection-redesign. Свои `webcams.*` с
18
+ тем же значением удалить (иначе замораживают перевод; `i18n:check` покажет их как dead overrides), отличающиеся —
19
+ оставить как override. SEO раздела (`meta.webcams.*`, `webcams_footer_*`) остаётся в сайте. Не сделать — ничего
20
+ не сломается.
21
+
22
+ - **Сайтам без сгруппированного вида категорий** (videosection-redesign): в `app.config` (через
23
+ `config/site.config.ts`) поставить `categoriesGrouped: false` — `CategoryNav` перестаёт рисовать табы
24
+ Grouped / A-Z, «All» в алфавите ведёт на `/categories`. После этого удалить проектный оверрайд
25
+ `components/category/nav.vue` (образец — коммит videosection-redesign с этим изменением). Не сделать — табы
26
+ останутся, пока жив оверрайд; без оверрайда табы вернутся. Остальным сайтам — ничего, по умолчанию `true`.
27
+
14
28
  ## 0.0.1033 — 2026-10-14
15
29
 
16
30
  - **Сайтам со своей темой (hdsex-remaster):** в `_theme.scss` добавить `$switcher-icon--live: $color-emerald-400;` и
@@ -1141,6 +1141,7 @@ $document-link: $color-indigo-400;
1141
1141
  $document-link--hover: $color-indigo-300;
1142
1142
 
1143
1143
  // ─── Вебкам-блок ───
1144
+ $webcam-main-bg: transparent;
1144
1145
  $webcam-icon-text: $color-white;
1145
1146
  $webcam-icon-bg: $color-white-10;
1146
1147
  $webcam-header-promo-text: $color-white;
@@ -1168,6 +1168,7 @@ $document-link: $color-indigo-400;
1168
1168
  $document-link--hover: $color-indigo-300;
1169
1169
 
1170
1170
  // ─── Вебкам-блок ───
1171
+ $webcam-main-bg: $color-gray-100;
1171
1172
  $webcam-icon-text: $color-gray-700;
1172
1173
  $webcam-icon-bg: $color-gray-200;
1173
1174
  $webcam-header-promo-text: $color-white;
@@ -87,6 +87,7 @@ const { t } = useI18n();
87
87
  color: $categories-link-text;
88
88
  margin-bottom: 6px;
89
89
  padding-block: 4px;
90
+ width: max-content;
90
91
  }
91
92
 
92
93
  @include to-br(sm) {
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <SectionNavLinks
3
+ v-if="isGrouped"
3
4
  :items="categoriesNavigation"
4
5
  theme="secondary"
5
6
  />
@@ -31,18 +32,20 @@ const props = defineProps<{
31
32
 
32
33
  const { t } = useI18n();
33
34
 
35
+ const isGrouped = useAppConfig().categoriesGrouped !== false;
36
+
34
37
  const alphabetItems = computed<IChipsItem[]>(() => [
35
38
  {
36
39
  title: t('all'),
37
40
  value: '',
38
- prefix: 'categories/all',
41
+ prefix: isGrouped ? 'categories/all' : 'categories',
39
42
  },
40
43
  ...(props.items || []),
41
44
  ]);
42
45
  </script>
43
46
 
44
47
  <style lang="scss">
45
- .category-nav__alphabet {
48
+ .section-nav-links + .category-nav__alphabet {
46
49
  margin-top: 16px;
47
50
  }
48
51
  </style>
@@ -0,0 +1,36 @@
1
+ <!-- Блок «Recommended to you»: заголовок и сетка роликов без вебкам-врезок; пустой список — блока нет -->
2
+ <template>
3
+ <section
4
+ v-if="items.length"
5
+ class="section-recommended"
6
+ >
7
+ <UiSectionTitle
8
+ :title="t('recommended_to_you')"
9
+ title-tag="h2"
10
+ variant="section"
11
+ />
12
+
13
+ <GridVideos
14
+ :items="items"
15
+ :inserts="[]"
16
+ />
17
+ </section>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import type { IVideoCard } from '../../types';
22
+
23
+ defineProps<{
24
+ items: IVideoCard[]
25
+ }>();
26
+
27
+ const { t } = useI18n();
28
+ </script>
29
+
30
+ <style lang="scss">
31
+ .section-recommended {
32
+ display: grid;
33
+ row-gap: 16px;
34
+ margin-block: 40px;
35
+ }
36
+ </style>
@@ -8,7 +8,11 @@
8
8
  v-if="isWebcamsEnabled"
9
9
  class="view-models__intro"
10
10
  >
11
- <h1 class="view-models__heading _title">{{ h1 }}</h1>
11
+ <UiSectionTitle
12
+ :title="h1"
13
+ :count="isFiltered || letter ? data?.total : modelsTotal || data?.total"
14
+ icon="user"
15
+ />
12
16
 
13
17
  <UiSwitcher
14
18
  :items="sections"
@@ -251,18 +255,12 @@ const topCards = computed(() => {
251
255
  }
252
256
 
253
257
  .view-models__intro {
254
- display: flex;
255
- flex-wrap: wrap;
256
- align-items: center;
257
- column-gap: 20px;
258
+ display: grid;
259
+ justify-items: start;
258
260
  row-gap: 16px;
259
261
  margin-bottom: 16px;
260
262
  }
261
263
 
262
- .view-models__heading {
263
- margin-bottom: 0;
264
- }
265
-
266
264
  .view-models__pagination {
267
265
  margin-top: 32px;
268
266
  }
@@ -16,7 +16,11 @@ query-параметрах `filter_*`. Формат провайдера при
16
16
  <div class="view-webcams _page _container">
17
17
  <header class="view-webcams__header">
18
18
  <div class="view-webcams__intro">
19
- <h1 class="view-webcams__title _title">{{ h1 }}</h1>
19
+ <UiSectionTitle
20
+ :title="h1"
21
+ :count="data?.total"
22
+ icon="webcam"
23
+ />
20
24
 
21
25
  <UiSwitcher
22
26
  :items="sections"
@@ -300,17 +304,11 @@ watch(() => JSON.stringify(toWebcamFilterPayload(route.query)), () => {
300
304
  }
301
305
 
302
306
  .view-webcams__intro {
303
- display: flex;
304
- flex-wrap: wrap;
305
- align-items: center;
306
- column-gap: 20px;
307
+ display: grid;
308
+ justify-items: start;
307
309
  row-gap: 16px;
308
310
  }
309
311
 
310
- .view-webcams__title {
311
- margin-bottom: 0;
312
- }
313
-
314
312
  .view-webcams__options {
315
313
  display: flex;
316
314
  flex-wrap: wrap;
@@ -115,4 +115,17 @@ onBeforeUnmount(() => {
115
115
  row-gap: 20px;
116
116
  margin-bottom: 40px;
117
117
  }
118
+
119
+ // Островок — только в теме, где у блока есть подложка; без неё блок как был, без отступов
120
+ @if $webcam-main-bg != transparent {
121
+ .webcam-main {
122
+ padding: 12px;
123
+ background-color: $webcam-main-bg;
124
+ border-radius: $radius-2xl;
125
+
126
+ @include from-br(sm) {
127
+ padding: 20px;
128
+ }
129
+ }
130
+ }
118
131
  </style>
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "Live Cams",
4
+ "online_now": "{count} online",
5
+ "language_online": "{lang} — {count} online",
6
+ "more": "Mehr",
7
+ "more_online": {
8
+ "straight": "Mehr Online-Girls",
9
+ "gay": "Mehr Online-Guys",
10
+ "shemale": "Mehr Online-Transsexuelle"
11
+ },
12
+ "new_model": "Neues Model",
13
+ "goal": "Ziel:",
14
+ "speaks": "Spricht {languages}",
15
+ "years_old": "{age} J.",
16
+ "live_cams_gay": "Gay Live Cams",
17
+ "live_cams_shemale": "Shemale Live Cams"
18
+ },
2
19
  "top_models": "Top-Models",
3
20
  "additional_link": {
4
21
  "2257": "2257",
@@ -1,4 +1,43 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "Live Cams",
4
+ "live_cams_gay": "Gay Live Cams",
5
+ "live_cams_shemale": "Shemale Live Cams",
6
+ "online_now": "{count} online now",
7
+ "language_online": "{lang} speaking - {count} online now",
8
+ "more": "More",
9
+ "more_online": {
10
+ "straight": "More Online Girls",
11
+ "gay": "More Online Guys",
12
+ "shemale": "More Online T-Girls"
13
+ },
14
+ "new_model": "New model",
15
+ "goal": "Goal:",
16
+ "speaks": "Speaks {languages}",
17
+ "speaking": "{lang} speaking",
18
+ "years_old": "{age} y.o.",
19
+ "online_only": "Online Only",
20
+ "online_short": "{count} online",
21
+ "sections": {
22
+ "videos": "Video",
23
+ "webcams": "Webcam"
24
+ },
25
+ "filters": {
26
+ "age": "Age",
27
+ "country": "Country",
28
+ "language": "Language",
29
+ "body_type": "Body Type",
30
+ "ethnicity": "Ethnicity",
31
+ "hair_color": "Hair Color",
32
+ "eye_color": "Eye Color",
33
+ "specifics": "Specifics",
34
+ "subculture": "Type"
35
+ },
36
+ "filter_groups": {
37
+ "appearance": "Appearance"
38
+ }
39
+ },
40
+ "recommended_to_you": "Recommended to you",
2
41
  "also_known_as_male": "Also known as:",
3
42
  "also_known_as_female": "Also known as:",
4
43
  "model_about": "About",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "Cams en directo",
4
+ "online_now": "{count} en línea",
5
+ "language_online": "{lang} — {count} en línea",
6
+ "more": "Más",
7
+ "more_online": {
8
+ "straight": "Más chicas en línea",
9
+ "gay": "Más chicos en línea",
10
+ "shemale": "Más trans en línea"
11
+ },
12
+ "new_model": "Modelo nueva",
13
+ "goal": "Meta:",
14
+ "speaks": "Habla {languages}",
15
+ "years_old": "{age} años",
16
+ "live_cams_gay": "Cams gay",
17
+ "live_cams_shemale": "Cams trans"
18
+ },
2
19
  "top_models": "Top modelos",
3
20
  "additional_link": {
4
21
  "2257": "2257",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "Cams en direct",
4
+ "online_now": "{count} en ligne",
5
+ "language_online": "{lang} — {count} en ligne",
6
+ "more": "Plus",
7
+ "more_online": {
8
+ "straight": "Plus de filles en ligne",
9
+ "gay": "Plus de mecs en ligne",
10
+ "shemale": "Plus de trans en ligne"
11
+ },
12
+ "new_model": "Nouveau modèle",
13
+ "goal": "Objectif :",
14
+ "speaks": "Parle {languages}",
15
+ "years_old": "{age} ans",
16
+ "live_cams_gay": "Cams gay",
17
+ "live_cams_shemale": "Cams trans"
18
+ },
2
19
  "top_models": "Top modèles",
3
20
  "additional_link": {
4
21
  "2257": "2257",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "Cam in diretta",
4
+ "online_now": "{count} online",
5
+ "language_online": "{lang} — {count} online",
6
+ "more": "Altro",
7
+ "more_online": {
8
+ "straight": "Altre ragazze online",
9
+ "gay": "Altri ragazzi online",
10
+ "shemale": "Altre trans online"
11
+ },
12
+ "new_model": "Nuova modella",
13
+ "goal": "Obiettivo:",
14
+ "speaks": "Parla {languages}",
15
+ "years_old": "{age} anni",
16
+ "live_cams_gay": "Cam gay",
17
+ "live_cams_shemale": "Cam trans"
18
+ },
2
19
  "top_models": "Top modelli",
3
20
  "additional_link": {
4
21
  "2257": "2257",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "ライブチャット",
4
+ "online_now": "{count}人がオンライン",
5
+ "language_online": "{lang} — {count}人オンライン",
6
+ "more": "もっと見る",
7
+ "more_online": {
8
+ "straight": "オンラインの女性をもっと見る",
9
+ "gay": "オンラインの男性をもっと見る",
10
+ "shemale": "オンラインのニューハーフをもっと見る"
11
+ },
12
+ "new_model": "新人モデル",
13
+ "goal": "目標:",
14
+ "speaks": "{languages}を話します",
15
+ "years_old": "{age}歳",
16
+ "live_cams_gay": "ゲイのライブチャット",
17
+ "live_cams_shemale": "ニューハーフのライブチャット"
18
+ },
2
19
  "top_models": "人気モデル",
3
20
  "additional_link": {
4
21
  "2257": "2257",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "라이브 캠",
4
+ "online_now": "{count}명 접속 중",
5
+ "language_online": "{lang} — {count}명 접속",
6
+ "more": "더 보기",
7
+ "more_online": {
8
+ "straight": "접속 중인 여성 더 보기",
9
+ "gay": "접속 중인 남성 더 보기",
10
+ "shemale": "접속 중인 트랜스 더 보기"
11
+ },
12
+ "new_model": "신규 모델",
13
+ "goal": "목표:",
14
+ "speaks": "{languages} 사용",
15
+ "years_old": "{age}세",
16
+ "live_cams_gay": "게이 라이브 캠",
17
+ "live_cams_shemale": "쉬메일 라이브 캠"
18
+ },
2
19
  "top_models": "인기 모델",
3
20
  "additional_link": {
4
21
  "2257": "2257",
@@ -1,4 +1,42 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "Трансляции",
4
+ "online_now": "{count} онлайн",
5
+ "language_online": "{lang} — {count} онлайн",
6
+ "more": "Ещё",
7
+ "more_online": {
8
+ "straight": "Больше девушек",
9
+ "gay": "Больше парней",
10
+ "shemale": "Больше трансов"
11
+ },
12
+ "new_model": "Новая модель",
13
+ "goal": "Цель:",
14
+ "speaks": "Говорит: {languages}",
15
+ "speaking": "Говорит: {lang}",
16
+ "years_old": "{age} лет",
17
+ "live_cams_gay": "Гей-трансляции",
18
+ "live_cams_shemale": "Шимейл-трансляции",
19
+ "online_only": "Только в эфире",
20
+ "online_short": "{count} в эфире",
21
+ "sections": {
22
+ "videos": "Видео",
23
+ "webcams": "Вебкам"
24
+ },
25
+ "filters": {
26
+ "age": "Возраст",
27
+ "country": "Страна",
28
+ "language": "Язык",
29
+ "body_type": "Телосложение",
30
+ "ethnicity": "Внешний тип",
31
+ "hair_color": "Цвет волос",
32
+ "eye_color": "Цвет глаз",
33
+ "specifics": "Особенности",
34
+ "subculture": "Тип"
35
+ },
36
+ "filter_groups": {
37
+ "appearance": "Внешность"
38
+ }
39
+ },
2
40
  "top_models": "Популярные модели",
3
41
  "additional_link": {
4
42
  "2257": "2257",
@@ -1,4 +1,21 @@
1
1
  {
2
+ "webcams": {
3
+ "live_cams": "真人视讯",
4
+ "online_now": "{count} 人在线",
5
+ "language_online": "{lang} — {count} 人在线",
6
+ "more": "更多",
7
+ "more_online": {
8
+ "straight": "更多在线女孩",
9
+ "gay": "更多在线男生",
10
+ "shemale": "更多在线人妖"
11
+ },
12
+ "new_model": "新模特",
13
+ "goal": "目标:",
14
+ "speaks": "会说 {languages}",
15
+ "years_old": "{age} 岁",
16
+ "live_cams_gay": "男同真人视讯",
17
+ "live_cams_shemale": "人妖真人视讯"
18
+ },
2
19
  "top_models": "热门模特",
3
20
  "additional_link": {
4
21
  "2257": "2257",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.1034",
4
+ "version": "0.0.1037",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -8,11 +8,11 @@
8
8
  <UiSectionTitle
9
9
  :title="h1"
10
10
  :count="categories?.length"
11
- class="_title"
11
+ class="_mb-16"
12
12
  icon="grid"
13
13
  />
14
14
 
15
- <div class="_nav-filter">
15
+ <div class="category-letter__nav _nav-filter">
16
16
  <CategoryNav
17
17
  v-if="categoriesGroupByLetter"
18
18
  :items="categoriesGroupByLetter as IChipsItem[]"
@@ -92,6 +92,10 @@ const breadcrumbs = [
92
92
  min-height: 30vh;
93
93
  }
94
94
 
95
+ .category-letter__nav:has(.section-nav-links) {
96
+ margin-top: 8px;
97
+ }
98
+
95
99
  .category-letter__title {
96
100
  margin-bottom: 12px;
97
101
  }
@@ -0,0 +1,25 @@
1
+ <!-- Плейлист без ролика в адресе (`/playlists/{id}`, в том числе со старого `/playlist/{id}`) — 301 на первый ролик -->
2
+ <template>
3
+ <div class="_loading"/>
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ import { PlaylistsApiService } from '../../../services/api/playlists.service';
8
+
9
+ const featureFlags = useRuntimeConfig().public.featureFlags;
10
+ const route = useRoute();
11
+ const { generateLink } = useGenerateLink();
12
+
13
+ const { data } = await useFetchPlaylistsGetVideosById(PlaylistsApiService.getVideosByPlaylist);
14
+
15
+ const first = data.value?.items?.[0];
16
+
17
+ if (!first) {
18
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' });
19
+ }
20
+
21
+ await navigateTo(
22
+ generateLink(`/playlists/${String(route.params['playlistId'])}/${first[featureFlags.FetchByUrlEnabled ? 'url' : 'id']}`),
23
+ { redirectCode: 301, replace: true },
24
+ );
25
+ </script>
@@ -136,6 +136,7 @@ export class VideosApiService {
136
136
  lang: Language,
137
137
  id: string,
138
138
  niche = NicheHelper.getCurrentNiche(),
139
+ limit = 20,
139
140
  ): Promise<string[]> {
140
141
  return await ApiHelper.fetch(
141
142
  `/videos/referer-phrases-by-id/${id}`,
@@ -147,6 +148,9 @@ export class VideosApiService {
147
148
  'X-Cache-Time': '259200',
148
149
  'Content-Type': 'application/json',
149
150
  },
151
+ params: {
152
+ limit,
153
+ },
150
154
  },
151
155
  useRuntimeConfig(),
152
156
  );
@@ -155,6 +159,7 @@ export class VideosApiService {
155
159
  lang: Language,
156
160
  id: string,
157
161
  niche = NicheHelper.getCurrentNiche(),
162
+ limit = 20,
158
163
  ): Promise<string[]> {
159
164
  return await ApiHelper.fetch(
160
165
  `/videos/referer-phrases-by-url/${id}`,
@@ -166,6 +171,9 @@ export class VideosApiService {
166
171
  'X-Cache-Time': '259200',
167
172
  'Content-Type': 'application/json',
168
173
  },
174
+ params: {
175
+ limit,
176
+ },
169
177
  },
170
178
  useRuntimeConfig(),
171
179
  );