itube-specs 0.0.953 → 0.0.955
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.
|
@@ -115,6 +115,13 @@
|
|
|
115
115
|
v-if="model.age"
|
|
116
116
|
class="card-webcam__age"
|
|
117
117
|
>{{ t('webcams.years_old', { age: model.age }) }}</span>
|
|
118
|
+
|
|
119
|
+
<!-- Страна — в строке имени справа (в макете на узких экранах её нет вовсе). -->
|
|
120
|
+
<a
|
|
121
|
+
v-if="model.country"
|
|
122
|
+
:href="`${nichePath}/webcams?filter_country=${model.country}`"
|
|
123
|
+
class="card-webcam__country"
|
|
124
|
+
>{{ countryName }}</a>
|
|
118
125
|
</div>
|
|
119
126
|
|
|
120
127
|
<!-- На узких экранах ховера нет, поэтому цель стримера с прогрессом живёт под
|
|
@@ -136,27 +143,6 @@
|
|
|
136
143
|
</span>
|
|
137
144
|
</div>
|
|
138
145
|
|
|
139
|
-
<div
|
|
140
|
-
v-if="model.viewers || model.country"
|
|
141
|
-
class="card-webcam__meta"
|
|
142
|
-
>
|
|
143
|
-
<span
|
|
144
|
-
v-if="model.viewers"
|
|
145
|
-
class="card-webcam__viewers"
|
|
146
|
-
>
|
|
147
|
-
<UiIcon
|
|
148
|
-
name="eye"
|
|
149
|
-
size="14"
|
|
150
|
-
/>{{ $n(model.viewers) }}
|
|
151
|
-
</span>
|
|
152
|
-
|
|
153
|
-
<a
|
|
154
|
-
v-if="model.country"
|
|
155
|
-
:href="`${nichePath}/webcams?filter_country=${model.country}`"
|
|
156
|
-
class="card-webcam__country _truncate"
|
|
157
|
-
>{{ countryName }}</a>
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
146
|
<ul
|
|
161
147
|
v-if="visibleTags.length"
|
|
162
148
|
class="card-webcam__tags"
|
|
@@ -484,8 +470,8 @@ onBeforeUnmount(() => {
|
|
|
484
470
|
opacity: 1;
|
|
485
471
|
}
|
|
486
472
|
|
|
487
|
-
// одна плашка на постере:
|
|
488
|
-
//
|
|
473
|
+
// одна плашка на постере: зрители онлайн и, у новых моделей, признак новизны перед
|
|
474
|
+
// ними (в макете это один пилюль на все разрешения)
|
|
489
475
|
.card-webcam__badge {
|
|
490
476
|
@include font-4xs;
|
|
491
477
|
|
|
@@ -499,8 +485,6 @@ onBeforeUnmount(() => {
|
|
|
499
485
|
padding: 2px 6px;
|
|
500
486
|
font-weight: 600;
|
|
501
487
|
line-height: 1;
|
|
502
|
-
text-transform: uppercase;
|
|
503
|
-
letter-spacing: .025em;
|
|
504
488
|
color: $card-webcam-badge-text;
|
|
505
489
|
text-shadow: 0 1px 2px $card-webcam-badge-shadow;
|
|
506
490
|
|
|
@@ -510,21 +494,16 @@ onBeforeUnmount(() => {
|
|
|
510
494
|
border-radius: $radius-small;
|
|
511
495
|
}
|
|
512
496
|
|
|
497
|
+
// капс с трекингом — только у признака новизны: цифры зрителей в макете набраны обычным
|
|
498
|
+
.card-webcam__badge-new {
|
|
499
|
+
text-transform: uppercase;
|
|
500
|
+
letter-spacing: .025em;
|
|
501
|
+
}
|
|
502
|
+
|
|
513
503
|
.card-webcam__badge-viewers {
|
|
514
504
|
display: flex;
|
|
515
505
|
align-items: center;
|
|
516
506
|
gap: 4px;
|
|
517
|
-
|
|
518
|
-
@include from-br(xm) {
|
|
519
|
-
display: none;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// на десктопе плашка нужна только под признак новой модели
|
|
524
|
-
.card-webcam__badge:not(:has(.card-webcam__badge-new)) {
|
|
525
|
-
@include from-br(xm) {
|
|
526
|
-
display: none;
|
|
527
|
-
}
|
|
528
507
|
}
|
|
529
508
|
|
|
530
509
|
.card-webcam__online {
|
|
@@ -651,43 +630,32 @@ onBeforeUnmount(() => {
|
|
|
651
630
|
}
|
|
652
631
|
|
|
653
632
|
.card-webcam__age {
|
|
654
|
-
@include font-sm;
|
|
655
|
-
|
|
656
|
-
flex-shrink: 0;
|
|
657
|
-
color: $card-webcam-meta-text;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// на узких экранах мету не показываем: зрители уехали в плашку на постере, а
|
|
661
|
-
// место под карточкой занимает цель стримера
|
|
662
|
-
.card-webcam__meta {
|
|
663
633
|
@include font-xs;
|
|
664
634
|
|
|
665
|
-
|
|
666
|
-
align-items: center;
|
|
667
|
-
justify-content: space-between;
|
|
668
|
-
gap: 8px;
|
|
669
|
-
margin-top: 8px;
|
|
635
|
+
flex-shrink: 0;
|
|
670
636
|
color: $card-webcam-meta-text;
|
|
671
|
-
|
|
672
|
-
@include from-br(xm) {
|
|
673
|
-
display: flex;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
.card-webcam__viewers {
|
|
678
|
-
display: flex;
|
|
679
|
-
align-items: center;
|
|
680
|
-
gap: 4px;
|
|
681
637
|
}
|
|
682
638
|
|
|
639
|
+
// обрезка своя, а не утилитой `_truncate`: та ставит `display` через !important и
|
|
640
|
+
// строку не спрятать на мобиле
|
|
683
641
|
.card-webcam__country {
|
|
684
|
-
|
|
642
|
+
@include font-xs;
|
|
685
643
|
|
|
644
|
+
flex-shrink: 0;
|
|
686
645
|
margin-left: auto;
|
|
646
|
+
overflow: hidden;
|
|
647
|
+
color: $card-webcam-meta-text;
|
|
648
|
+
text-overflow: ellipsis;
|
|
649
|
+
white-space: nowrap;
|
|
687
650
|
|
|
688
651
|
&:is(:hover, :focus) {
|
|
689
652
|
color: $card-webcam-meta-text--hover;
|
|
690
653
|
}
|
|
654
|
+
|
|
655
|
+
// на узких экранах строку имени занимают флаги, имя и возраст
|
|
656
|
+
@include to-br(xm) {
|
|
657
|
+
display: none;
|
|
658
|
+
}
|
|
691
659
|
}
|
|
692
660
|
|
|
693
661
|
// селектор с родителем не случайно: ресет обнуляет отступы у `ul[class]`, и
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
height="14"
|
|
33
33
|
loading="lazy"
|
|
34
34
|
>
|
|
35
|
-
{{ t('webcams.language_online', { lang:
|
|
35
|
+
{{ t('webcams.language_online', { lang: languageName, count: $n(languageCount) }) }}
|
|
36
36
|
</span>
|
|
37
37
|
</div>
|
|
38
38
|
</template>
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<script setup lang="ts">
|
|
41
41
|
import type { IWebcamCounts } from '../../types';
|
|
42
42
|
import { LANG_TO_FLAG } from '../../utils/webcams/lang-to-flag';
|
|
43
|
+
import { isoCodeToLanguageName } from '../../utils/iso-code-to-language-name';
|
|
43
44
|
|
|
44
45
|
const props = defineProps<{
|
|
45
46
|
counts: IWebcamCounts
|
|
@@ -51,6 +52,8 @@ const total = computed(() => props.counts.total || 0);
|
|
|
51
52
|
const languageCode = computed(() => locale.value.slice(0, 2).toLowerCase());
|
|
52
53
|
const languageFlag = computed(() => LANG_TO_FLAG[languageCode.value] || languageCode.value);
|
|
53
54
|
const languageCount = computed(() => props.counts.byLanguages?.[languageCode.value] || 0);
|
|
55
|
+
// язык называем по-человечески («English speaking»), а не кодом («EN speaking») — как в фильтрах
|
|
56
|
+
const languageName = computed(() => isoCodeToLanguageName(languageCode.value, locale.value));
|
|
54
57
|
</script>
|
|
55
58
|
|
|
56
59
|
<style lang="scss">
|
|
@@ -7,12 +7,22 @@ import { useRoute } from 'vue-router';
|
|
|
7
7
|
export const useFetchVideos = async (
|
|
8
8
|
apiMethod: (...args: any[]) => Promise<PaginatedResponse<IVideoCard>>,
|
|
9
9
|
// defaultOrder всегда непустой: в запрос уходит либо выбранная сортировка, либо дефолтная
|
|
10
|
-
{ sortItems = sortItemsDefault, defaultOrder = '-country_order'
|
|
10
|
+
{ sortItems = sortItemsDefault, defaultOrder = '-country_order', perPage: perPageOption }: {
|
|
11
|
+
sortItems?: INavLinkItem[],
|
|
12
|
+
defaultOrder?: string,
|
|
13
|
+
/**
|
|
14
|
+
* Сколько роликов просить у API. По умолчанию 48 на десктопе и 12 на мобиле —
|
|
15
|
+
* ровно ячейки сетки. Переопределяют там, где часть ячеек занимает не видео
|
|
16
|
+
* (врезки вебкам-тумб): тогда роликов просят меньше на число врезок, и сетка
|
|
17
|
+
* остаётся кратной колонкам, без неполной строки.
|
|
18
|
+
*/
|
|
19
|
+
perPage?: { mobile?: number, desktop?: number },
|
|
20
|
+
} = {}
|
|
11
21
|
) => {
|
|
12
22
|
const route = useRoute();
|
|
13
23
|
|
|
14
24
|
const mobile = !!useState<boolean>('isMobile').value;
|
|
15
|
-
const perPage = mobile ? 12 : 48;
|
|
25
|
+
const perPage = (mobile ? perPageOption?.mobile : perPageOption?.desktop) ?? (mobile ? 12 : 48);
|
|
16
26
|
const lang = useLang() as Language;
|
|
17
27
|
const filters = computed(() => useGetVideosFilterRequest(route, selectDurationItems, selectAddedItems));
|
|
18
28
|
const key = AsyncData.Videos;
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isoCodeToLanguageName } from '../iso-code-to-language-name';
|
|
3
|
+
|
|
4
|
+
describe('isoCodeToLanguageName', () => {
|
|
5
|
+
it('отдаёт название языка на своей локали, с заглавной', () => {
|
|
6
|
+
expect(isoCodeToLanguageName('en', 'en')).toBe('English');
|
|
7
|
+
expect(isoCodeToLanguageName('en', 'ru')).toBe('Английский');
|
|
8
|
+
expect(isoCodeToLanguageName('ru', 'ru')).toBe('Русский');
|
|
9
|
+
expect(isoCodeToLanguageName('de', 'de')).toBe('Deutsch');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('регистр и пробелы в коде не мешают', () => {
|
|
13
|
+
expect(isoCodeToLanguageName(' EN ', 'en')).toBe('English');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('нераспознанный код возвращает кодом, пустой — пустым', () => {
|
|
17
|
+
expect(isoCodeToLanguageName('zz', 'en')).toBe('ZZ');
|
|
18
|
+
expect(isoCodeToLanguageName('', 'en')).toBe('');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 639-1 → название языка на текущей локали средствами ICU: `en` → «English» /
|
|
3
|
+
* «английский», `zh` → «Chinese». Тот же словарь, что у браузеров и ОС, своей таблицы
|
|
4
|
+
* держать не надо.
|
|
5
|
+
*
|
|
6
|
+
* Первая буква приводится к верхнему регистру: у ru/it/es ICU отдаёт название со строчной
|
|
7
|
+
* («русский»), а подпись — начало фразы («Русский — 5 470 онлайн»).
|
|
8
|
+
*
|
|
9
|
+
* Нераспознанный код возвращается кодом в верхнем регистре: подпись вида «EN speaking»
|
|
10
|
+
* лучше пустой, а по возвращённому значению проверять существование языка нельзя.
|
|
11
|
+
*/
|
|
12
|
+
export const isoCodeToLanguageName = (isoCode: string, locale: string): string => {
|
|
13
|
+
const code = (isoCode || '').trim().toLowerCase();
|
|
14
|
+
if (!code) return '';
|
|
15
|
+
|
|
16
|
+
const fallback = code.toUpperCase();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const name = new Intl.DisplayNames([locale], { type: 'language' }).of(code);
|
|
20
|
+
// ICU на неизвестном коде возвращает сам код
|
|
21
|
+
if (!name || name.toLowerCase() === code) return fallback;
|
|
22
|
+
|
|
23
|
+
return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
|
|
24
|
+
} catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
};
|