itube-specs 0.0.954 → 0.0.956
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.
|
@@ -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">
|
|
@@ -29,12 +29,22 @@
|
|
|
29
29
|
import type { IStripcashModel, IWebcamsConfig } from '../../types';
|
|
30
30
|
import { WEBCAMS_DEFAULTS } from '../../runtime';
|
|
31
31
|
|
|
32
|
-
const props = defineProps<{
|
|
33
|
-
/** Сколько
|
|
32
|
+
const props = withDefaults(defineProps<{
|
|
33
|
+
/** Сколько тумб показывает сам блок; по умолчанию — из конфига сайта. */
|
|
34
34
|
limit?: number
|
|
35
35
|
/** Категория/тег провайдера — для тематических блоков. */
|
|
36
36
|
category?: string
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Сколько моделей из ХВОСТА списка забирает страница — например под врезки в
|
|
39
|
+
* сетку видео. Блок просит у провайдера `limit + reserveTail` и показывает
|
|
40
|
+
* только свою голову, поэтому одни и те же модели не повторяются дважды на
|
|
41
|
+
* странице (правило старого движка: список приходит один и делится на части).
|
|
42
|
+
* По умолчанию 0 — блок ведёт себя как раньше.
|
|
43
|
+
*/
|
|
44
|
+
reserveTail?: number
|
|
45
|
+
}>(), {
|
|
46
|
+
reserveTail: 0,
|
|
47
|
+
});
|
|
38
48
|
|
|
39
49
|
/**
|
|
40
50
|
* Клики и показы тумб блок отдаёт наружу: индексы он знает, а вот куда их
|
|
@@ -47,11 +57,16 @@ const emit = defineEmits<{
|
|
|
47
57
|
|
|
48
58
|
const webcamsConfig = computed(() => (useRuntimeConfig().public.webcamsConfig || {}) as IWebcamsConfig);
|
|
49
59
|
|
|
60
|
+
const blockLimit = computed(() => props.limit || webcamsConfig.value.limit || WEBCAMS_DEFAULTS.limit);
|
|
61
|
+
|
|
50
62
|
const { items, counts, isEnabled } = await useFetchWebcams({
|
|
51
|
-
limit:
|
|
63
|
+
limit: blockLimit.value + props.reserveTail,
|
|
52
64
|
category: props.category,
|
|
53
65
|
});
|
|
54
66
|
|
|
67
|
+
/** Хвост списка отдан странице (врезки в сетку) — блок берёт только свою голову. */
|
|
68
|
+
const blockItems = computed(() => items.value.slice(0, blockLimit.value));
|
|
69
|
+
|
|
55
70
|
/**
|
|
56
71
|
* Колонок в сетке столько же, сколько в основном гриде сайта на этом
|
|
57
72
|
* брейкпоинте. На сервере ширины нет, поэтому там 0 — «пока не знаем».
|
|
@@ -67,9 +82,9 @@ const columns = ref(0);
|
|
|
67
82
|
*/
|
|
68
83
|
const visibleItems = computed<IStripcashModel[]>(() => {
|
|
69
84
|
const cols = columns.value;
|
|
70
|
-
if (!cols) return
|
|
85
|
+
if (!cols) return blockItems.value;
|
|
71
86
|
|
|
72
|
-
return
|
|
87
|
+
return blockItems.value.slice(0, Math.floor(blockItems.value.length / cols) * cols);
|
|
73
88
|
});
|
|
74
89
|
|
|
75
90
|
// те же пороги, что у основного грида (`grid-videos`): xs → 3 колонки, md → 4
|
|
@@ -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
|
+
};
|