itube-specs 0.0.1070 → 0.0.1071
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 +16 -0
- package/assets/scss/themes/dark/_theme.scss +8 -0
- package/assets/scss/themes/light/_theme.scss +8 -0
- package/components/adv/faphouse/index.vue +4 -1
- package/components/card/category.vue +1 -0
- package/components/card/channel.vue +1 -0
- package/components/card/collections-more.vue +1 -0
- package/components/card/playlist-mini.vue +1 -0
- package/components/card/playlist.vue +2 -0
- package/components/card/video/index.vue +5 -1
- package/components/card/video/mini.vue +1 -0
- package/components/card/webcam/index.vue +4 -1
- package/components/info/main.vue +1 -0
- package/components/layout/age-banner.vue +108 -0
- package/components/library/feed.vue +1 -0
- package/components/library/item.vue +1 -0
- package/components/library/upload-card.vue +1 -0
- package/components/player/channel.vue +1 -0
- package/components/player/core.vue +10 -3
- package/components/player/follow.vue +1 -0
- package/components/player/sponsored.vue +1 -0
- package/components/ui/avatar.vue +6 -2
- package/components/ui/chips.vue +1 -0
- package/components/ui/img.vue +49 -1
- package/composables/use-age-gate.ts +16 -0
- package/i18n/locales/en-US.json +7 -0
- package/layouts/default.vue +2 -0
- package/middleware/age.global.ts +8 -10
- package/package.json +1 -1
- package/runtime/utils/server/age.ts +1 -1
- package/server/api/age/url.get.ts +11 -0
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,22 @@
|
|
|
11
11
|
коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
|
|
12
12
|
кому именно (всем / сайтам с N), что будет, если не сделать.
|
|
13
13
|
|
|
14
|
+
## 0.0.1071 — 2026-11-03
|
|
15
|
+
|
|
16
|
+
- **Сайтам со своей темой в `assets/scss/vars/_theme.scss` (hd-sex-new), срочно:** без новых токенов сборка падает
|
|
17
|
+
на `Undefined variable`. Добавить `$layout-age-banner-bg`, `$layout-age-banner-title`, `$layout-age-banner-text`,
|
|
18
|
+
`$layout-age-banner-btn-bg`, `$layout-age-banner-btn-bg--hover`, `$layout-age-banner-btn-text`, `$ui-img-gate-bg`,
|
|
19
|
+
`$ui-img-gate-text` — значения по аналогии с `themes/dark|light/_theme.scss` слоя.
|
|
20
|
+
- **Сайтам с `FeatureAgeVerificationEnabled` — к сведению:** верификация больше не редиректит на agerify при заходе,
|
|
21
|
+
а тестовый `NL` в списке стран заменён на `GB`.
|
|
22
|
+
Пока она не пройдена, превью (видео, категории, каналы, плейлисты, вебкамы, FapHouse, аватары каналов и
|
|
23
|
+
пользователей, картинки в чипсах, логотипы спонсоров, картинка в шапке категории/канала) показываются
|
|
24
|
+
заглушкой «18+» (не закрыты только портреты моделей), ховер-превью не играют, под шапкой липкая плашка с кнопкой
|
|
25
|
+
верификации (`LayoutAgeBanner`). Свой `UiImg` со взрослым превью — ставить проп `adult`. Плеер при этом заблокирован (сплошной фон без постера, текст и
|
|
26
|
+
кнопка верификации, без рекламы) — нужен `itube-modern-player` ≥ 0.16.0 (`npm i itube-modern-player@^0.16.0`), на
|
|
27
|
+
старом плеер гейт не покажет и ролик будет играть. Новая группа общих ключей `age_gate` (пока только en-US).
|
|
28
|
+
Сайтам без флага делать ничего не надо.
|
|
29
|
+
|
|
14
30
|
## 0.0.1068 — 2026-11-03
|
|
15
31
|
|
|
16
32
|
- **Всем — решить флаг:** верификация возраста (редирект на agerify, пока не пройдена) снова работает, за флагом
|
|
@@ -33,6 +33,14 @@ $layout-header-border: $color-zinc-800-30;
|
|
|
33
33
|
$layout-nav-border: $color-zinc-800-50;
|
|
34
34
|
$layout-skip-link-bg: $color-zinc-800-60;
|
|
35
35
|
$layout-skip-link-text: $color-white;
|
|
36
|
+
$layout-age-banner-bg: $color-emerald-600;
|
|
37
|
+
$layout-age-banner-title: $color-white;
|
|
38
|
+
$layout-age-banner-text: $color-white-90;
|
|
39
|
+
$layout-age-banner-btn-bg: $color-white;
|
|
40
|
+
$layout-age-banner-btn-bg--hover: $color-white-90;
|
|
41
|
+
$layout-age-banner-btn-text: $color-green-900;
|
|
42
|
+
$ui-img-gate-bg: $color-zinc-800;
|
|
43
|
+
$ui-img-gate-text: $color-zinc-500;
|
|
36
44
|
|
|
37
45
|
// ─── Page footer links (mixin) ───
|
|
38
46
|
$page-footer-links-border: $color-zinc-800;
|
|
@@ -44,6 +44,14 @@ $layout-header-border: transparent;
|
|
|
44
44
|
$layout-nav-border: $color-slate-750;
|
|
45
45
|
$layout-skip-link-bg: $color-slate-750;
|
|
46
46
|
$layout-skip-link-text: $color-white;
|
|
47
|
+
$layout-age-banner-bg: $color-emerald-600;
|
|
48
|
+
$layout-age-banner-title: $color-white;
|
|
49
|
+
$layout-age-banner-text: $color-white-90;
|
|
50
|
+
$layout-age-banner-btn-bg: $color-white;
|
|
51
|
+
$layout-age-banner-btn-bg--hover: $color-white-90;
|
|
52
|
+
$layout-age-banner-btn-text: $color-green-900;
|
|
53
|
+
$ui-img-gate-bg: $color-gray-200;
|
|
54
|
+
$ui-img-gate-text: $color-gray-500;
|
|
47
55
|
|
|
48
56
|
// ─── Page footer links (mixin) ───
|
|
49
57
|
$page-footer-links-border: transparent;
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
<UiImg
|
|
40
40
|
:src="item.imageUrl"
|
|
41
41
|
:alt="item.title"
|
|
42
|
+
adult
|
|
42
43
|
class="adv-faphouse__img"
|
|
43
44
|
width="335"
|
|
44
45
|
height="188"
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
loading="lazy"
|
|
47
48
|
/>
|
|
48
49
|
<video
|
|
49
|
-
v-if="previewIndex === index && item.previewUrl"
|
|
50
|
+
v-if="previewIndex === index && item.previewUrl && !isAgeGated"
|
|
50
51
|
:src="item.previewUrl"
|
|
51
52
|
class="adv-faphouse__video"
|
|
52
53
|
muted
|
|
@@ -84,6 +85,8 @@ const { items } = isEnabled.value ? await useFetchFaphouse() : { items: computed
|
|
|
84
85
|
const isMobile = useState<boolean>('isMobile');
|
|
85
86
|
const previewIndex = ref(-1);
|
|
86
87
|
|
|
88
|
+
const { isAgeGated } = useAgeGate();
|
|
89
|
+
|
|
87
90
|
const onPreviewShow = (index: number) => {
|
|
88
91
|
if (isMobile.value) return;
|
|
89
92
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
:alt="card.name"
|
|
21
21
|
:loading="priority ? 'eager' : 'lazy'"
|
|
22
22
|
:priority="priority"
|
|
23
|
+
adult
|
|
23
24
|
class="card-playlist__img"
|
|
24
25
|
@error="onImageError(index)"
|
|
25
26
|
/>
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
v-if="avatar"
|
|
84
85
|
:src="avatar"
|
|
85
86
|
:alt="card.username"
|
|
87
|
+
adult
|
|
86
88
|
class="card-collections-more__count _from-xs"
|
|
87
89
|
width="36"
|
|
88
90
|
height="36"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
:priority="priority"
|
|
27
27
|
:alt="card.title"
|
|
28
28
|
sizes="auto, (min-width: 1440px) 25vw, (min-width: 900px) 33vw, (min-width: 370px) 50vw, 100vw"
|
|
29
|
+
adult
|
|
29
30
|
class="card-video__poster"
|
|
30
31
|
width="480"
|
|
31
32
|
height="270"
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
:loading="loading"
|
|
79
80
|
:src="currentAvatar"
|
|
80
81
|
:alt="card.channelName"
|
|
82
|
+
adult
|
|
81
83
|
class="card-video__channel-logo"
|
|
82
84
|
width="44"
|
|
83
85
|
height="44"
|
|
@@ -174,7 +176,9 @@ const { generateLink } = useGenerateLink();
|
|
|
174
176
|
const previewId = useId();
|
|
175
177
|
const { activePreviewId, startPreviewByHover, stopPreviewByHover, startPreviewByTouch } = useVideoPreview();
|
|
176
178
|
|
|
177
|
-
const
|
|
179
|
+
const { isAgeGated } = useAgeGate();
|
|
180
|
+
|
|
181
|
+
const isPreviewShow = computed(() => activePreviewId.value === previewId && !isAgeGated.value);
|
|
178
182
|
|
|
179
183
|
const posterError = ref(false);
|
|
180
184
|
const avatarError = ref(false);
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
<UiImg
|
|
30
30
|
:src="poster"
|
|
31
31
|
:alt="model.name"
|
|
32
|
+
adult
|
|
32
33
|
class="card-webcam__img"
|
|
33
34
|
width="335"
|
|
34
35
|
height="188"
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
/>
|
|
39
40
|
|
|
40
41
|
<video
|
|
41
|
-
v-if="isPreviewActive && model.previewUrl"
|
|
42
|
+
v-if="isPreviewActive && model.previewUrl && !isAgeGated"
|
|
42
43
|
ref="videoRef"
|
|
43
44
|
:class="{'--ready': isPreviewReady}"
|
|
44
45
|
class="card-webcam__video"
|
|
@@ -216,6 +217,8 @@ const isPreviewActive = previewSlot.isActive(previewId.value);
|
|
|
216
217
|
// видео прозрачным, чтобы поверх постера не висел чёрный прямоугольник
|
|
217
218
|
const isPreviewReady = ref(false);
|
|
218
219
|
|
|
220
|
+
const { isAgeGated } = useAgeGate();
|
|
221
|
+
|
|
219
222
|
/** Ниша в пути: у straight префикса нет, у остальных — `/gay`, `/shemale`. */
|
|
220
223
|
/**
|
|
221
224
|
* Возраст в тумбе — ссылка на раздел с фильтром возраста ±1 год: так делал боевой
|
package/components/info/main.vue
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!-- Плашка под шапкой, пока возрастная верификация требуется и не пройдена (useAgeGate) -->
|
|
2
|
+
<template>
|
|
3
|
+
<section
|
|
4
|
+
v-if="isAgeGated"
|
|
5
|
+
class="layout-age-banner"
|
|
6
|
+
>
|
|
7
|
+
<div class="layout-age-banner__inner _container">
|
|
8
|
+
<p class="layout-age-banner__content">
|
|
9
|
+
<strong class="layout-age-banner__title">{{ t('age_gate.title') }}</strong>
|
|
10
|
+
<span class="layout-age-banner__text">{{ t('age_gate.text') }}</span>
|
|
11
|
+
</p>
|
|
12
|
+
<UiBtn
|
|
13
|
+
:disabled="isPending"
|
|
14
|
+
:title="t('age_gate.button')"
|
|
15
|
+
theme="primary"
|
|
16
|
+
size="l"
|
|
17
|
+
class="layout-age-banner__btn"
|
|
18
|
+
@click="onVerify"
|
|
19
|
+
>{{ t('age_gate.button') }}</UiBtn>
|
|
20
|
+
</div>
|
|
21
|
+
</section>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
const { t } = useI18n();
|
|
26
|
+
const route = useRoute();
|
|
27
|
+
const { isAgeGated, startVerification } = useAgeGate();
|
|
28
|
+
|
|
29
|
+
const isPending = ref(false);
|
|
30
|
+
|
|
31
|
+
const onVerify = async () => {
|
|
32
|
+
isPending.value = true;
|
|
33
|
+
try {
|
|
34
|
+
await startVerification(route.fullPath);
|
|
35
|
+
} finally {
|
|
36
|
+
isPending.value = false;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<style lang="scss">
|
|
42
|
+
.layout-age-banner {
|
|
43
|
+
position: sticky;
|
|
44
|
+
top: var(--header-height);
|
|
45
|
+
z-index: calc(#{$z-index-header} - 2);
|
|
46
|
+
padding-block: 10px;
|
|
47
|
+
background-color: $layout-age-banner-bg;
|
|
48
|
+
|
|
49
|
+
@include from-br($brkpnt-burger) {
|
|
50
|
+
$layout-nav-height: 59px;
|
|
51
|
+
|
|
52
|
+
top: calc(var(--header-height) + #{$layout-nav-height});
|
|
53
|
+
padding-block: 12px;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.layout-age-banner__inner {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 12px;
|
|
62
|
+
text-align: center;
|
|
63
|
+
|
|
64
|
+
@include from-br(sm) {
|
|
65
|
+
flex-direction: row;
|
|
66
|
+
justify-content: space-between;
|
|
67
|
+
gap: 32px;
|
|
68
|
+
text-align: left;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.layout-age-banner__content {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: 4px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.layout-age-banner__title {
|
|
79
|
+
@include font-base;
|
|
80
|
+
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
color: $layout-age-banner-title;
|
|
83
|
+
|
|
84
|
+
@include from-br(sm) {
|
|
85
|
+
@include font-lg;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.layout-age-banner__text {
|
|
90
|
+
@include font-sm;
|
|
91
|
+
|
|
92
|
+
color: $layout-age-banner-text;
|
|
93
|
+
|
|
94
|
+
@include from-br(sm) {
|
|
95
|
+
@include font-base;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.layout-age-banner__btn {
|
|
100
|
+
--button-bg: #{$layout-age-banner-btn-bg};
|
|
101
|
+
--button-bg--hover: #{$layout-age-banner-btn-bg--hover};
|
|
102
|
+
--button-text: #{$layout-age-banner-btn-text};
|
|
103
|
+
--button-border: transparent;
|
|
104
|
+
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
font-weight: 700;
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
@menurequest="onMenuRequest"
|
|
21
21
|
@pause="onPause"
|
|
22
22
|
@ready="siteHooks.onReady?.()"
|
|
23
|
+
@blockedclick="startVerification(route.fullPath)"
|
|
23
24
|
>
|
|
24
25
|
<template
|
|
25
26
|
v-if="siteHooks.pauseScreen"
|
|
@@ -79,6 +80,9 @@ const emit = defineEmits<{
|
|
|
79
80
|
|
|
80
81
|
const { t, n, locale } = useI18n();
|
|
81
82
|
|
|
83
|
+
const route = useRoute();
|
|
84
|
+
const { isAgeGated, startVerification } = useAgeGate();
|
|
85
|
+
|
|
82
86
|
const siteHooks = usePlayerSiteHooks();
|
|
83
87
|
const isPauseScreen = computed(() => !!siteHooks.pauseScreen?.enabled.value);
|
|
84
88
|
|
|
@@ -206,7 +210,7 @@ const currentSource = computed<VideoSource>(() => {
|
|
|
206
210
|
type: 'application/x-mpegurl',
|
|
207
211
|
title: props.data.title,
|
|
208
212
|
description: props.data.description,
|
|
209
|
-
poster: src
|
|
213
|
+
poster: src && !isAgeGated.value
|
|
210
214
|
? {
|
|
211
215
|
src,
|
|
212
216
|
fallback,
|
|
@@ -217,7 +221,7 @@ const currentSource = computed<VideoSource>(() => {
|
|
|
217
221
|
alt: props.data.title,
|
|
218
222
|
}
|
|
219
223
|
: undefined,
|
|
220
|
-
thumbnails: props.data.vttUrl,
|
|
224
|
+
thumbnails: isAgeGated.value ? undefined : props.data.vttUrl,
|
|
221
225
|
// Главы: плеер сам рисует кнопку сцен, подписи и превью из VTT.
|
|
222
226
|
// ITimestamp.duration — момент старта главы (секунды).
|
|
223
227
|
// `end_duration` бекенд отдаёт не всегда — без него плеер досчитывает конец сам
|
|
@@ -251,7 +255,7 @@ function nextPreviewMeta(card: IVideoCard): PreviewMetaItem[] {
|
|
|
251
255
|
// externalAdvance плеер его не грузит (реальный стрим придёт по ?view из main.vue).
|
|
252
256
|
const nextSource = computed<VideoSource | null>(() => {
|
|
253
257
|
const card = props.nextVideo;
|
|
254
|
-
if (!card) return null;
|
|
258
|
+
if (!card || isAgeGated.value) return null;
|
|
255
259
|
|
|
256
260
|
const poster = card.thumbUrls?.webp?.[ThumbSize.Big] || card.thumbUrls?.webp?.[ThumbSize.Medium];
|
|
257
261
|
|
|
@@ -333,11 +337,13 @@ const options = computed<Omit<PlayerOptions, 'source'>>(() => ({
|
|
|
333
337
|
labels: { sceneTypes: t('jump_to') },
|
|
334
338
|
// Запоминаем громкость/mute в localStorage.
|
|
335
339
|
persist: { key: PLAYER_PERSIST_KEY },
|
|
340
|
+
blocked: isAgeGated.value ? { text: t('age_gate.player_text'), button: t('age_gate.button') } : undefined,
|
|
336
341
|
}));
|
|
337
342
|
|
|
338
343
|
// Async-фабрика: обёртка вызывает её параллельно загрузке ядра и конструирует плеер
|
|
339
344
|
// с готовым adConfig — нам не нужно синхронизировать VAST-запрос с инициализацией.
|
|
340
345
|
async function loadAdConfig(): Promise<AdsOptions | undefined> {
|
|
346
|
+
if (isAgeGated.value) return undefined;
|
|
341
347
|
try {
|
|
342
348
|
const vastTag = await AdvertsHelper.getPrerollVastLink(AdvertsHelper.getPrerollSpotType());
|
|
343
349
|
if (vastTag && vastTag.trim() !== '') {
|
|
@@ -353,6 +359,7 @@ async function loadAdConfig(): Promise<AdsOptions | undefined> {
|
|
|
353
359
|
<style lang="scss">
|
|
354
360
|
.player-core {
|
|
355
361
|
position: relative;
|
|
362
|
+
isolation: isolate;
|
|
356
363
|
aspect-ratio: 16/9;
|
|
357
364
|
overflow: hidden;
|
|
358
365
|
width: 100%;
|
package/components/ui/avatar.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ui-avatar">
|
|
3
3
|
<img
|
|
4
|
-
v-if="src || data.avatar"
|
|
4
|
+
v-if="(src || data.avatar) && !isAgeGated"
|
|
5
5
|
:src="avatar"
|
|
6
6
|
:width="size"
|
|
7
7
|
:height="size"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
v-else
|
|
12
12
|
:style="{ width: size + 'px', height: size + 'px' }"
|
|
13
13
|
class="ui-avatar__placeholder"
|
|
14
|
-
>{{ avatar }}</span>
|
|
14
|
+
>{{ isAgeGated ? initial : avatar }}</span>
|
|
15
15
|
</div>
|
|
16
16
|
</template>
|
|
17
17
|
|
|
@@ -30,6 +30,10 @@ const props = withDefaults(defineProps<{
|
|
|
30
30
|
const data = computed(() => useState<IProfileData>('profileData').value);
|
|
31
31
|
|
|
32
32
|
const avatar = computed(() => props.src || (data.value && (data.value.avatar || data.value.username.charAt(0))));
|
|
33
|
+
|
|
34
|
+
const initial = computed(() => data.value?.username?.charAt(0) || '');
|
|
35
|
+
|
|
36
|
+
const { isAgeGated } = useAgeGate();
|
|
33
37
|
</script>
|
|
34
38
|
|
|
35
39
|
<style lang="scss">
|
package/components/ui/chips.vue
CHANGED
package/components/ui/img.vue
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
<!-- `<picture>` только когда есть avif: обёртка сдвигает картинку на уровень ниже, а
|
|
3
3
|
`card/playlist.vue` и `playlist-mini.vue` целятся в свои UiImg через `:first-child`/`:nth-child(2)`
|
|
4
4
|
— внутри `<picture>` каждая из них стала бы первым ребёнком, и раскладка поехала бы. -->
|
|
5
|
+
<span
|
|
6
|
+
v-if="adult && isAgeGated"
|
|
7
|
+
v-bind="attrs"
|
|
8
|
+
:style="{ aspectRatio: `${width} / ${height}`, '--ui-img-gate-width': `${width}px` }"
|
|
9
|
+
class="ui-img-gate"
|
|
10
|
+
>
|
|
11
|
+
<span class="ui-img-gate__label">{{ $t('age_gate.thumb') }}</span>
|
|
12
|
+
</span>
|
|
5
13
|
<picture
|
|
6
|
-
v-if="avifSrcset"
|
|
14
|
+
v-else-if="avifSrcset"
|
|
7
15
|
class="ui-img"
|
|
8
16
|
>
|
|
9
17
|
<source
|
|
@@ -41,6 +49,8 @@ const props = withDefaults(defineProps<{
|
|
|
41
49
|
loading?: 'lazy' | 'eager',
|
|
42
50
|
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside',
|
|
43
51
|
priority?: boolean,
|
|
52
|
+
/** Взрослое превью: при возрастном гейте вместо картинки — заглушка. Портреты моделей и аватары — без него. */
|
|
53
|
+
adult?: boolean,
|
|
44
54
|
}>(), {
|
|
45
55
|
loading: 'lazy',
|
|
46
56
|
fit: 'cover',
|
|
@@ -48,6 +58,8 @@ const props = withDefaults(defineProps<{
|
|
|
48
58
|
|
|
49
59
|
const attrs = useAttrs();
|
|
50
60
|
|
|
61
|
+
const { isAgeGated } = useAgeGate();
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* `sizes="auto"` точнее любого авторского списка, но молодой: Chrome 126, Firefox 150
|
|
53
65
|
* (апрель 2026), Safari 27 (сентябрь 2026). Где не поддержан — деградирует в `100vw`,
|
|
@@ -102,4 +114,40 @@ const emit = defineEmits<{
|
|
|
102
114
|
.ui-img source {
|
|
103
115
|
display: none;
|
|
104
116
|
}
|
|
117
|
+
|
|
118
|
+
:where(.ui-img-gate) {
|
|
119
|
+
position: relative;
|
|
120
|
+
display: block;
|
|
121
|
+
width: var(--ui-img-gate-width);
|
|
122
|
+
max-width: 100%;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.ui-img-gate {
|
|
126
|
+
container-type: inline-size;
|
|
127
|
+
background-color: $ui-img-gate-bg;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ui-img-gate__label {
|
|
131
|
+
@include font-lg;
|
|
132
|
+
|
|
133
|
+
position: absolute;
|
|
134
|
+
inset: 0;
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
color: $ui-img-gate-text;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@include to-cq(160px) {
|
|
143
|
+
.ui-img-gate__label {
|
|
144
|
+
@include font-xs;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@include to-cq(56px) {
|
|
149
|
+
.ui-img-gate__label {
|
|
150
|
+
@include font-4xs;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
105
153
|
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Возрастной гейт: верификация в регионе посетителя требуется и ещё не пройдена. Состояние
|
|
3
|
+
* ставит middleware/age.global.ts на сервере; пока оно включено, взрослые превью заменяются
|
|
4
|
+
* заглушкой, плеер заблокирован, под шапкой плашка. `startVerification` уводит на agerify и
|
|
5
|
+
* возвращает на текущую страницу.
|
|
6
|
+
*/
|
|
7
|
+
export const useAgeGate = () => {
|
|
8
|
+
const isAgeGated = useState<boolean>('age-gate', () => false);
|
|
9
|
+
|
|
10
|
+
const startVerification = async (backPath: string) => {
|
|
11
|
+
const { url } = await $fetch<{ url: string }>('/bff/age/url', { query: { back: backPath } });
|
|
12
|
+
if (url) window.location.href = url;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return { isAgeGated, startVerification };
|
|
16
|
+
};
|
package/i18n/locales/en-US.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
+
"age_gate": {
|
|
3
|
+
"thumb": "18+",
|
|
4
|
+
"title": "Laws in your region require age verification to view 18+ content",
|
|
5
|
+
"text": "Takes a minute, and we keep no personal data",
|
|
6
|
+
"button": "Verify age",
|
|
7
|
+
"player_text": "Confirm your age to watch this video"
|
|
8
|
+
},
|
|
2
9
|
"faphouse": {
|
|
3
10
|
"promo_text": "❤️ Watch and download FULL videos in HD & 4K quality 🔥",
|
|
4
11
|
"promo_action": "GET ACCESS"
|
package/layouts/default.vue
CHANGED
package/middleware/age.global.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isGeoMatch,
|
|
1
|
+
import { isGeoMatch, isAgeVerified } from '../runtime';
|
|
2
|
+
import { useAgeGate } from '../composables/use-age-gate';
|
|
2
3
|
|
|
3
|
-
export default defineNuxtRouteMiddleware(async (
|
|
4
|
+
export default defineNuxtRouteMiddleware(async () => {
|
|
4
5
|
if (!import.meta.server) return;
|
|
5
|
-
if (to.path.startsWith('/bff/age/verify')) return;
|
|
6
6
|
|
|
7
7
|
const config = useRuntimeConfig();
|
|
8
8
|
if (!config.public.featureFlags?.FeatureAgeVerificationEnabled) return;
|
|
@@ -10,12 +10,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
10
10
|
const event = useRequestEvent();
|
|
11
11
|
if (!(await isGeoMatch(event, config))) return;
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return navigateTo(url, { external: true });
|
|
19
|
-
}
|
|
13
|
+
const { isAgeGated } = useAgeGate();
|
|
14
|
+
try {
|
|
15
|
+
isAgeGated.value = !(await isAgeVerified(event, config));
|
|
16
|
+
} catch {
|
|
17
|
+
isAgeGated.value = true;
|
|
20
18
|
}
|
|
21
19
|
});
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { getClientHeaders } from './get-client-headers';
|
|
|
4
4
|
/** Возвращает true если пользователь находится в регионе где требуется верификация возраста. */
|
|
5
5
|
export async function isGeoMatch(event, _config) {
|
|
6
6
|
const AGE_VERIFY_REGIONS: string[] = [];
|
|
7
|
-
const AGE_VERIFY_COUNTRIES = ['
|
|
7
|
+
const AGE_VERIFY_COUNTRIES = ['GB'];
|
|
8
8
|
|
|
9
9
|
const headers = getClientHeaders(event);
|
|
10
10
|
const countryCode = headers[ 'countryCode' ];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getQuery } from 'h3';
|
|
2
|
+
import { getAgeValidationUrl } from '../../../runtime';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const back = String(getQuery(event).back || '/');
|
|
6
|
+
const backPath = back.startsWith('/') && !back.startsWith('//') ? back : '/';
|
|
7
|
+
|
|
8
|
+
const { url } = await getAgeValidationUrl(event, backPath, useRuntimeConfig(event));
|
|
9
|
+
|
|
10
|
+
return { url };
|
|
11
|
+
});
|