itube-specs 0.0.951 → 0.0.952

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.
@@ -74,7 +74,7 @@
74
74
 
75
75
  <span
76
76
  v-if="model.goal"
77
- :class="{'--visible': isPreviewActive}"
77
+ :class="{'--visible': isGoalVisible}"
78
78
  class="card-webcam__goal"
79
79
  >
80
80
  <span class="card-webcam__goal-text _truncate">
@@ -100,12 +100,15 @@
100
100
  :niche-path="nichePath"
101
101
  />
102
102
 
103
+ <!-- Ховер по имени показывает только цель: превью — дело постера. -->
103
104
  <a
104
105
  :href="model.url"
105
106
  class="card-webcam__name _truncate"
106
107
  target="_blank"
107
108
  rel="nofollow noopener sponsored"
108
109
  @click="onClick"
110
+ @mouseenter="onNameEnter"
111
+ @mouseleave="onNameLeave"
109
112
  >{{ model.name }}</a>
110
113
 
111
114
  <span
@@ -315,6 +318,24 @@ function onLeave() {
315
318
  previewSlot.release(previewId.value);
316
319
  }
317
320
 
321
+ /**
322
+ * Цель стримера появляется в двух случаях: при превью (ховер по постеру) и просто
323
+ * при ховере по имени модели — там поток не нужен, а прочитать цель хочется.
324
+ */
325
+ const isNameHovered = ref(false);
326
+
327
+ const isGoalVisible = computed(() => isPreviewActive.value || isNameHovered.value);
328
+
329
+ function onNameEnter() {
330
+ if (props.touchPreview) return;
331
+
332
+ isNameHovered.value = true;
333
+ }
334
+
335
+ function onNameLeave() {
336
+ isNameHovered.value = false;
337
+ }
338
+
318
339
  /**
319
340
  * Тач: превью включает удержание тумбы — короткий тап должен открывать модель, а
320
341
  * не запускать поток. Движение пальцем считаем скроллом и удержание отменяем.
@@ -118,6 +118,14 @@ const props = defineProps<{
118
118
  /** Где живёт состояние: свой query-параметр `filter_<name>` (модели, вебкамы) или общая
119
119
  * строка `categories=<группа>_<значение>` (каталог видео, синхронно с попапом фильтра). */
120
120
  mode?: 'param' | 'categories'
121
+ /**
122
+ * Своя подпись ВЫБРАННОГО значения на кнопке селекта. Без пропса — обычное поведение
123
+ * (название выбранной опции). Строка — шаблон с `{value}` (например `{value} speaking`),
124
+ * функция получает фильтр и выбранную опцию и может вернуть пусто — тогда подпись
125
+ * стандартная. Нужна там, где выбранное значение читается фразой: в разделе вебкамов
126
+ * язык на кнопке и в чипсе выглядит как «Russian speaking».
127
+ */
128
+ selectedLabel?: string | ((filter: IModelFilter, option: IModelFilterOptions) => string | undefined)
121
129
  }>();
122
130
 
123
131
  const route = useRoute();
@@ -300,7 +308,18 @@ function getSelectLabel(item: IModelFilter) {
300
308
  const current = readQueryValue(item.name, route.query);
301
309
  if (!current) return item.title;
302
310
  const opt = item.options.find(o => o.name === current);
303
- return opt ? capitalize(opt.title) : item.title;
311
+ if (!opt) return item.title;
312
+
313
+ return overrideSelectedLabel(item, opt) || capitalize(opt.title);
314
+ }
315
+
316
+ /** Подпись выбранного значения из пропса `selectedLabel`; пусто — берётся стандартная. */
317
+ function overrideSelectedLabel(item: IModelFilter, option: IModelFilterOptions) {
318
+ if (!props.selectedLabel) return '';
319
+
320
+ if (typeof props.selectedLabel === 'function') return props.selectedLabel(item, option) || '';
321
+
322
+ return props.selectedLabel.replace('{value}', capitalize(option.title));
304
323
  }
305
324
 
306
325
  // Количество бек считает не для всех опций (у синтетической «all» его нет) — скобки без числа не рисуем
@@ -53,6 +53,7 @@ query-параметрах `filter_*`. Формат провайдера при
53
53
  <FilterSelects
54
54
  v-if="filters.length"
55
55
  :filters="filters"
56
+ :selected-label="selectedFilterLabel"
56
57
  />
57
58
  </div>
58
59
  </div>
@@ -114,11 +115,11 @@ query-параметрах `filter_*`. Формат провайдера при
114
115
  </template>
115
116
 
116
117
  <script setup lang="ts">
117
- import type { IModelCard, IModelFilter, ISwitcherItem, IStripcashModel, IWebcamModelsResponse, PaginatedResponse } from '../../types';
118
+ import type { IModelCard, IModelFilter, IModelFilterOptions, ISwitcherItem, IStripcashModel, IWebcamModelsResponse, PaginatedResponse } from '../../types';
118
119
  import { countWebcamFilters } from '../../utils/webcams/count-filters';
119
120
  import { normalizeWebcamFacets } from '../../utils/webcams/normalize-facets';
120
121
  import { toWebcamFilterPayload } from '../../utils/webcams/to-filter-payload';
121
- import { WEBCAM_FILTER_ANY } from '../../utils/webcams/facets';
122
+ import { WEBCAM_FILTER_ANY, WEBCAM_LANGUAGE_FACET } from '../../utils/webcams/facets';
122
123
  import { sortWebcams } from '../../lib';
123
124
 
124
125
  const props = defineProps<{
@@ -181,6 +182,18 @@ const filters = computed<IModelFilter[]>(() => (currentFacets.value.length ? cur
181
182
  group: { ...filter.group, title: translate(`webcams.filter_groups.${filter.group.name}`, filter.group.name) },
182
183
  })));
183
184
 
185
+ /**
186
+ * Выбранный язык на кнопке селекта читается фразой — «Russian speaking», как в макете
187
+ * и как в чипсе облака фильтров. Остальные фильтры стандартные: у них значение и так
188
+ * самодостаточное («Украина», «18-24»). Ключа у сайта может не быть — тогда останется
189
+ * просто название языка.
190
+ */
191
+ function selectedFilterLabel(filter: IModelFilter, option: IModelFilterOptions) {
192
+ if (filter.name !== WEBCAM_LANGUAGE_FACET || !te('webcams.speaking')) return '';
193
+
194
+ return t('webcams.speaking', { lang: option.title });
195
+ }
196
+
184
197
  /** Модалка фильтра слоя ждёт ответ листинга — ей нужны только фасеты. */
185
198
  const filterData = computed(() => ({ filters: filters.value }) as unknown as PaginatedResponse<IModelCard>);
186
199
 
@@ -3,11 +3,9 @@ import type { LocationQuery } from '#vue-router';
3
3
  import { getMonth } from '../runtime';
4
4
  import { buildChipParts } from '../utils/filter-chips/build-chip-parts';
5
5
  import { LANG_TO_FLAG } from '../utils/webcams/lang-to-flag';
6
+ import { WEBCAM_LANGUAGE_FACET } from '../utils/webcams/facets';
6
7
  import type { Ref } from 'vue';
7
8
 
8
- /** Единственный фильтр-язык у нас в вебкамах: у моделей такого поля нет. */
9
- const LANGUAGE_FILTER = 'language';
10
-
11
9
  export function useFilterChipsItems(
12
10
  filters: Ref<IModelFilter[]>,
13
11
  route: { query: LocationQuery },
@@ -95,7 +93,7 @@ export function useFilterChipsItems(
95
93
  * флага. Если у сайта ключа перевода ещё нет, остаётся название языка.
96
94
  */
97
95
  const buildLanguageChip = (valueText: string) => {
98
- const code = String(route.query[`filter_${LANGUAGE_FILTER}`] || '').toLowerCase();
96
+ const code = String(route.query[`filter_${WEBCAM_LANGUAGE_FACET}`] || '').toLowerCase();
99
97
 
100
98
  return {
101
99
  title: t('webcams.speaking', { lang: valueText }) || valueText,
@@ -110,7 +108,7 @@ export function useFilterChipsItems(
110
108
  const valueText = getValueText(item);
111
109
  if (!valueText) return null;
112
110
  return {
113
- ...(filter.name === LANGUAGE_FILTER
111
+ ...(filter.name === WEBCAM_LANGUAGE_FACET
114
112
  ? buildLanguageChip(valueText)
115
113
  : buildChipParts(filter.title, valueText)),
116
114
  value: item.map(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.951",
4
+ "version": "0.0.952",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -24,6 +24,9 @@ export const WEBCAM_QUICK_FACETS = ['age', 'language', 'country', 'specifics'];
24
24
  /** Значение селекта «любой» — слой по нему удаляет параметр из query. */
25
25
  export const WEBCAM_FILTER_ANY = 'all';
26
26
 
27
+ /** Имя языкового фасета: у него подпись выбранного значения читается фразой («Russian speaking»). */
28
+ export const WEBCAM_LANGUAGE_FACET = 'language';
29
+
27
30
  /** Группы фильтров: строка фильтров и всё остальное. */
28
31
  export const WEBCAM_FILTER_GROUPS = {
29
32
  quick: { name: 'quick', order: 0 },