BongaCams, Chaturbate, Stripchat: Saubere Oberfläche, PiP, Vollbild, Aufnahme, Screenshots, Favoriten und vieles mehr.
// ==UserScript== // @name Cams Unchained // @name:ru Cams Unchained // @name:uk Cams Unchained // @name:es Cams Unchained // @name:de Cams Unchained // @name:fr Cams Unchained // @name:pt Cams Unchained // @name:it Cams Unchained // @name:pl Cams Unchained // @name:tr Cams Unchained // @name:nl Cams Unchained // @name:cs Cams Unchained // @name:ro Cams Unchained // @name:hu Cams Unchained // @name:bg Cams Unchained // @name:el Cams Unchained // @name:ja Cams Unchained // @name:zh-CN Cams Unchained // @name:ko Cams Unchained // @name:he Cams Unchained // @name:ar Cams Unchained // @name:hi Cams Unchained // @name:id Cams Unchained // @name:vi Cams Unchained // @name:th Cams Unchained // @namespace DiadiaBonga // @version 4.5.0 // @description:ru BongaCams, Chaturbate, Stripchat: Чистый интерфейс, PiP, полный экран, запись, скриншоты, закладки и многое другое. // @description BongaCams, Chaturbate, Stripchat: Clean interface, PiP, fullscreen, recording, screenshots, bookmarks and more. // @description:uk BongaCams, Chaturbate, Stripchat: Чистий інтерфейс, PiP, повний екран, запис, скріншоти, закладки та багато іншого. // @description:es BongaCams, Chaturbate, Stripchat: Interfaz limpia, PiP, pantalla completa, grabación, capturas, favoritos y mucho más. // @description:de BongaCams, Chaturbate, Stripchat: Saubere Oberfläche, PiP, Vollbild, Aufnahme, Screenshots, Favoriten und vieles mehr. // @description:fr BongaCams, Chaturbate, Stripchat: Interface épurée, PiP, plein écran, enregistrement, captures d'écran, favoris et bien plus. // @description:pt BongaCams, Chaturbate, Stripchat: Interface limpa, PiP, tela cheia, gravação, capturas de tela, favoritos e muito mais. // @description:it BongaCams, Chaturbate, Stripchat: Interfaccia pulita, PiP, schermo intero, registrazione, screenshot, preferiti e molto altro. // @description:pl BongaCams, Chaturbate, Stripchat: Czysty interfejs, PiP, pełny ekran, nagrywanie, zrzuty ekranu, zakładki i wiele więcej. // @description:tr BongaCams, Chaturbate, Stripchat: Temiz arayüz, PiP, tam ekran, kayıt, ekran görüntüsü, yer imleri ve daha fazlası. // @description:nl BongaCams, Chaturbate, Stripchat: Schone interface, PiP, volledig scherm, opnemen, screenshots, favorieten en meer. // @description:cs BongaCams, Chaturbate, Stripchat: Čisté rozhraní, PiP, celá obrazovka, nahrávání, snímky obrazovky, záložky a mnoho dalšího. // @description:ro BongaCams, Chaturbate, Stripchat: Interfață curată, PiP, ecran complet, înregistrare, capturi de ecran, favorite și multe altele. // @description:hu BongaCams, Chaturbate, Stripchat: Tiszta felület, PiP, teljes képernyő, felvétel, képernyőképek, kedvencek és még sok más. // @description:bg BongaCams, Chaturbate, Stripchat: Чист интерфейс, PiP, цял екран, запис, скриншоти, любими и още много. // @description:el BongaCams, Chaturbate, Stripchat: Καθαρή διεπαφή, PiP, πλήρης οθόνη, εγγραφή, στιγμιότυπα, αγαπημένα και πολλά άλλα. // @description:ja BongaCams, Chaturbate, Stripchat: クリーンなインターフェース、PiP、全画面、録画、スクリーンショット、ブックマークなど。 // @description:zh-CN BongaCams, Chaturbate, Stripchat: 简洁界面、PiP、全屏、录制、截图、收藏等多种功能。 // @description:ko BongaCams, Chaturbate, Stripchat: 깔끔한 인터페이스, PiP, 전체 화면, 녹화, 스크린샷, 즐겨찾기 등. // @description:he BongaCams, Chaturbate, Stripchat: ממשק נקי, PiP, מסך מלא, הקלטה, צילומי מסך, מועדפים ועוד. // @description:ar BongaCams, Chaturbate, Stripchat: واجهة نظيفة، PiP، ملء الشاشة، تسجيل، لقطات شاشة، مفضلة والمزيد. // @description:hi BongaCams, Chaturbate, Stripchat: साफ़ इंटरफ़ेस, PiP, फुलस्क्रीन, रिकॉर्डिंग, स्क्रीनशॉट, पसंदीदा और बहुत कुछ। // @description:id BongaCams, Chaturbate, Stripchat: Antarmuka bersih, PiP, layar penuh, perekaman, tangkapan layar, favorit, dan lainnya. // @description:vi BongaCams, Chaturbate, Stripchat: Giao diện gọn gàng, PiP, toàn màn hình, ghi hình, chụp màn hình, yêu thích và nhiều hơn nữa. // @description:th BongaCams, Chaturbate, Stripchat: อินเทอร์เฟซสะอาด, PiP, เต็มจอ, อัดวิดีโอ, แคปหน้าจอ, รายการโปรด และอื่นๆ // @author DiaDiaBogDan // @license MIT // @antifeature referral-link // @match *://*.bongacams.com/* // @match *://*.bongacams2.com/* // @match *://*.bongocams.com/* // @match *://*.webcamsluts.ru/* // @match *://*.stripchat.com/* // @match *://stripchat.com/* // @match *://*.chaturbate.com/* // @match *://chaturbate.com/* // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMCIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzJiMmIzMCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyMTIxNSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJoYWxvIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAiIHkxPSItNCIgeDI9IjAiIHkyPSI1MDgiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzE4MTgxYiIvPjxzdG9wIG9mZnNldD0iLjQyIiBzdG9wLWNvbG9yPSIjMTgxODFiIi8+PHN0b3Agb2Zmc2V0PSIuNTgiIHN0b3AtY29sb3I9InJnYigyOC41LDI4LjUsMzIuMzQpIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJyZ2IoMTgsMTgsMjEpIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImhlYXJ0IiB4MT0iMC4xIiB5MT0iMCIgeDI9IjAuOSIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmOWRiNSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2Q0MmQ2OCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5rIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZlM2VhIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmY5ZmI4Ii8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9Im92ZXIiPjxyZWN0IHg9Ii0xMzAiIHk9Ii02NCIgd2lkdGg9IjI2MCIgaGVpZ2h0PSI2NCIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjcm9zcyI+PHJlY3QgeD0iLTUyIiB5PSItNjQiIHdpZHRoPSIxMDQiIGhlaWdodD0iNjQiLz48L2NsaXBQYXRoPjxwYXRoIGlkPSJoIiBkPSJNMjU2IDM5OCBDMTUyIDMyMiA5MCAyNjIgOTAgMTk4IEM5MCAxNTAgMTI2IDExNiAxNzAgMTE2IEMyMTAgMTE2IDI0MiAxMzggMjU2IDE3MiBDMjcwIDEzOCAzMDIgMTE2IDM0MiAxMTYgQzM4NiAxMTYgNDIyIDE1MCA0MjIgMTk4IEM0MjIgMjYyIDM2MCAzMjIgMjU2IDM5OCBaIi8+PHJlY3QgaWQ9ImwiIHg9Ii04OCIgeT0iLTM4IiB3aWR0aD0iMTc2IiBoZWlnaHQ9Ijc2IiByeD0iMzgiLz48bWFzayBpZD0iY3V0IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSItMTAiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTIwIj48cmVjdCB4PSIwIiB5PSItMTAiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTIwIiBmaWxsPSIjZmZmIi8+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzY4IDMyMikgcm90YXRlKDQ1KSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2Nyb3NzKSI+PHVzZSB4bGluazpocmVmPSIjbCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjUwIi8+PC9nPjwvZz48L21hc2s+PC9kZWZzPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiByeD0iMTEyIiBmaWxsPSJ1cmwoI2JnKSIvPjxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSI1MDYiIGhlaWdodD0iNTA2IiByeD0iMTA5IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIuMDciIHN0cm9rZS13aWR0aD0iMiIvPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgNCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM2OCAzMjIpIHJvdGF0ZSg0NSkiPjx1c2UgeGxpbms6aHJlZj0iI2wiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNsaW5rKSIgc3Ryb2tlLXdpZHRoPSIzMCIvPjwvZz48ZyBtYXNrPSJ1cmwoI2N1dCkiPjx1c2UgeGxpbms6aHJlZj0iI2giIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNoYWxvKSIgc3Ryb2tlLXdpZHRoPSI2NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjx1c2UgeGxpbms6aHJlZj0iI2giIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNoZWFydCkiIHN0cm9rZS13aWR0aD0iNDIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzY4IDMyMikgcm90YXRlKDQ1KSI+PGcgY2xpcC1wYXRoPSJ1cmwoI292ZXIpIj48dXNlIHhsaW5rOmhyZWY9IiNsIiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjbGluaykiIHN0cm9rZS13aWR0aD0iMzAiLz48L2c+PC9nPjwvZz48L3N2Zz4= // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @grant unsafeWindow // @grant GM_registerMenuCommand // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js // @connect bongacams.com // @connect bongacams2.com // @connect bongocams.com // @connect webcamsluts.ru // @connect stripchat.com // @connect chaturbate.com // @connect mmcdn.com // @connect doppiocdn.com // @connect doppiocdn.media // @connect doppiocdn.net // @connect doppiocdn.org // @connect bgicdn.com // @connect bcvcdn.com // @connect bestcam.tv // @connect camshowrecordings.com // @connect camshowrecord.net // @connect livecamsrip.com // @connect camdownload.net // @connect camshow.cc // @connect cdncontent.top // @connect pornotorrent.top // @connect pornolab.net // @connect simpcity.cr // @connect camsclips.com // @noframes // @run-at document-start // ==/UserScript== (function () { 'use strict'; const DEBUG = false; function dbg(err, where) { if (!DEBUG) return; try { console.debug('[Cams Unchained]', where || '', err); } catch (_) { } } const ICON_CLOSE = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>'; const realHidden = () => { try { return Object.getOwnPropertyDescriptor(Document.prototype, 'hidden').get.call(document); } catch { return document.hidden; } }; const BG_SUBS = { visibilitychange: [], pagehide: [] }; const onPageEvent = (type, fn, target = document) => { target.addEventListener(type, fn, true); BG_SUBS[type].push(fn); }; let streamUrl = null, streamPath = ''; const previewDirs = new Set(); const hlsDir = u => { try { const x = new URL(u); return x.origin + x.pathname.slice(0, x.pathname.lastIndexOf('/') + 1); } catch { return ''; } }; const hlsIsVariantOf = (u, master) => { const d = hlsDir(u), m = hlsDir(master); return !!d && !!m && d.length > m.length && d.startsWith(m); }; window.addEventListener('bc_hls', e => { const u = e.detail; for (const d of previewDirs) if (String(u).startsWith(d)) return; if (streamUrl && streamPath === location.pathname && hlsIsVariantOf(u, streamUrl)) return; streamUrl = u; streamPath = location.pathname; }); const getStreamUrl = () => (streamUrl && streamPath === location.pathname) ? streamUrl : null; const installNetHooks = () => { const s = document.createElement('script'); s.textContent = `try{const bcHost=/bongacams|bongocams|webcamsluts/.test(location.hostname);const emit=u=>{try{u=String(u);if(u.includes('.m3u8')&&!u.includes('chunk'))window.dispatchEvent(new CustomEvent('bc_hls',{detail:new URL(u,location.href).href}))}catch{}};const o=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(m,u){emit(u);return o.apply(this,arguments)};const f=window.fetch;if(f)window.fetch=function(i){try{emit(i&&i.url?i.url:i)}catch{}return f.apply(this,arguments)};if(bcHost&&window.WebSocket&&!window.__bcWS){window.__bcWS=1;var pend={},myId=2e9,live=[],last=0,chatWs=null,os=WebSocket.prototype.send;var fire=function(r){try{var u=+r.totalUsers||0,g=+r.totalGuests||0;last=Date.now();window.dispatchEvent(new CustomEvent('bc_viewers',{detail:JSON.stringify({n:u+g,u:u,g:g,t:last})}))}catch(x){}};WebSocket.prototype.send=function(d){try{if(typeof d==='string'&&d.indexOf('ChatModule.')>0){var j=JSON.parse(d);if(j&&typeof j.name==='string'&&j.name.indexOf('ChatModule.')===0){chatWs=this;if(j.name==='ChatModule.syncUserList')pend[j.id]=1}}}catch(x){}return os.apply(this,arguments)};var hook=function(ws){live.push(new WeakRef(ws));ws.addEventListener('message',function(e){try{var d=e.data;if(typeof d!=='string')return;if(d.indexOf('TOPIC_CHANGE')>0){var q=JSON.parse(d);if(q&&q.type==='ServerMessageEvent:TOPIC_CHANGE'&&q.body&&typeof q.body.topic==='string')window.dispatchEvent(new CustomEvent('bc_topic',{detail:JSON.stringify({t:q.body.topic})}));return}if(d.indexOf('totalUsers')<0)return;var j=JSON.parse(d);if(!j||j.id==null||!j.result||j.result.totalUsers==null)return;if(pend[j.id]){delete pend[j.id];fire(j.result)}else if(j.id>=2e9){e.stopImmediatePropagation();fire(j.result)}}catch(x){}})};window.WebSocket=new Proxy(window.WebSocket,{construct:function(t,a,nt){var ws=Reflect.construct(t,a,nt);hook(ws);return ws}});setInterval(function(){try{if(Date.now()-last<45000)return;var w=chatWs;if(!w||w.readyState!==1)return;os.call(w,JSON.stringify({id:++myId,name:'ChatModule.syncUserList',args:['public-chat']}))}catch(x){}},15000)}}catch{}`; (document.head || document.documentElement).appendChild(s); s.remove(); }; installNetHooks(); const installMediaHook = () => { const s = document.createElement('script'); s.textContent = `(()=>{try{if(window.__bcMH)return;window.__bcMH=1; var refs=[],ctxs=[],dead=false; var reg=function(e){if(!e)return e;if(refs.length>=300)refs=refs.filter(function(r){return r.deref()});if(refs.length<300&&refs.every(function(r){return r.deref()!==e}))refs.push(new WeakRef(e));return e}; var P=HTMLMediaElement.prototype,op=P.play; P.play=function(){reg(this);if(dead){try{this.pause()}catch(x){}return Promise.resolve()}return op.apply(this,arguments)}; var kk=false; ['src','srcObject'].forEach(function(n){var d=Object.getOwnPropertyDescriptor(P,n);if(d&&d.set){var st=d.set;Object.defineProperty(P,n,{configurable:true,enumerable:d.enumerable,get:d.get,set:function(v){if(dead&&!kk)return;return st.call(this,v)}})}}); var sa=Element.prototype.setAttribute; Element.prototype.setAttribute=function(n){if(dead&&this instanceof HTMLMediaElement&&String(n).toLowerCase()==='src')return;return sa.apply(this,arguments)}; var ce=Document.prototype.createElement; Document.prototype.createElement=function(t){var e=ce.apply(this,arguments);if(/^(video|audio)$/i.test(String(t)))reg(e);return e}; var OA=window.Audio;if(OA){var NA=function(){var e=arguments.length?new OA(arguments[0]):new OA();reg(e);return e};NA.prototype=OA.prototype;window.Audio=NA} var A=window.AudioContext||window.webkitAudioContext; if(A){var R=A.prototype.resume;A.prototype.resume=function(){if(ctxs.indexOf(this)<0)ctxs.push(this);if(dead)return Promise.resolve();return R.apply(this,arguments)}; ['createMediaElementSource','createMediaStreamSource','createBufferSource'].forEach(function(n){var f=A.prototype[n];if(f)A.prototype[n]=function(){if(ctxs.indexOf(this)<0)ctxs.push(this);return f.apply(this,arguments)}})} var kill=function(){dead=true;kk=true;refs.forEach(function(r){var e=r.deref();if(!e)return;try{e.pause();var m=e.srcObject;if(m&&m.getTracks)m.getTracks().forEach(function(t){t.stop()});e.srcObject=null;e.removeAttribute('src');e.load();e.remove()}catch(x){}});ctxs.forEach(function(c){try{c.suspend()}catch(x){}});kk=false}; window.addEventListener('bc_sleep_kill',kill) }catch(e){}})();`; (document.head || document.documentElement).appendChild(s); s.remove(); }; installMediaHook(); const COUNT_DIGIT_BASES = [0x660, 0x6F0, 0x966, 0xE50, 0xFF10]; const COUNT_SUF = { k: 1e3, 'к': 1e3, 'тыс': 1e3, 'тис': 1e3, 'tys': 1e3, 'tis': 1e3, 'mil': 1e3, '千': 1e3, '천': 1e3, m: 1e6, 'м': 1e6, 'млн': 1e6, 'mln': 1e6, 'mio': 1e6, '万': 1e4, '萬': 1e4, '만': 1e4 }; function parseCount(v) { const t = String(v == null ? '' : v).toLowerCase().replace(/\p{Nd}/gu, ch => { const c = ch.codePointAt(0); for (const b of COUNT_DIGIT_BASES) if (c >= b && c < b + 10) return String(c - b); return ch; }); const m = t.match(/(\d{1,3}(?:[\s\u00a0\u202f\u066c.,'’]\d{3})+(?!\d)|\d+(?:[.,]\d+)?)\s?(\p{L}+)?/u); if (!m) return NaN; const raw = m[1], word = (m[2] || '').replace(/\.$/, ''); let mul = COUNT_SUF[word]; if (!mul && word && '万萬만千천'.includes(word[0])) mul = COUNT_SUF[word[0]]; const grouped = /^\d{1,3}([\s\u00a0\u202f\u066c.,'’]\d{3})+$/.test(raw); let n; if (mul) n = parseFloat(raw.replace(/[\s\u00a0\u202f'’]/g, '').replace(',', '.')) * mul; else if (grouped) n = parseInt(raw.replace(/\D/g, ''), 10); else n = parseFloat(raw.replace(',', '.')); return Number.isFinite(n) ? Math.round(n) : NaN; } function fmtCount(n) { n = Math.floor(+n); if (!Number.isFinite(n) || n < 0) return ''; const lang = String(AppState.lang || 'EN').toUpperCase(); let div = 1, suf = ''; const cjk = { JA: ['万', '億'], ZH: ['万', '亿'], KO: ['만', '억'] }[lang]; if (cjk) { if (n >= 1e8) { div = 1e8; suf = cjk[1]; } else if (n >= 1e4) { div = 1e4; suf = cjk[0]; } } else { const cyr = lang === 'RU' || lang === 'UK' || lang === 'BG'; if (n >= 1e6) { div = 1e6; suf = cyr ? 'М' : 'M'; } else if (n >= 1e3) { div = 1e3; suf = cyr ? 'к' : 'k'; } } if (!suf) return String(n); const v = Math.floor(n / div * 10) / 10; if (v >= 100) return Math.floor(v) + suf; let s; try { s = v.toLocaleString(lang.toLowerCase() + '-u-nu-latn', { maximumFractionDigits: 1 }); } catch { s = String(v); } return s + suf; } const STORE_SITE = (() => { const h = location.hostname; return /bongacams|bongocams|webcamsluts/.test(h) ? 'bc' : /stripchat/.test(h) ? 'sc' : /chaturbate/.test(h) ? 'cb' : h; })(); const SHARED_KEY = /^(bc_bookmarks|bc_hist|bc_bm_|bc_cal_|bc_plat$|bc_lang|bc_bk_parts$|bc_mig_|bc_previews$|bc_prev_spd$|bc_favnav$)/; const siteKeyOf = k => (SHARED_KEY.test(k) ? k : `${STORE_SITE}:${k}`); const Store = { key: siteKeyOf, get: (k, def) => { try { const sk = siteKeyOf(k); let v = GM_getValue(sk); if (v == null && sk !== k) v = GM_getValue(k); if (v == null) return def; const p = JSON.parse(v); return (Array.isArray(def) && !Array.isArray(p)) || p === null ? def : p; } catch { return def; } }, set: (k, v) => { try { const sk = siteKeyOf(k); const str = JSON.stringify(v); GM_setValue(sk, str); try { const back = GM_getValue(sk); if (back != null && back !== str) return false; } catch (e) { dbg(e, 'set'); } return true; } catch { return false; } } }; const clamp = (v, min, max) => Math.max(min, Math.min(v, max)); let bcPtrDown = false; ['pointerdown'].forEach(t => window.addEventListener(t, () => { bcPtrDown = true; }, { capture: true, passive: true })); ['pointerup', 'pointercancel'].forEach(t => window.addEventListener(t, () => { bcPtrDown = false; }, { capture: true, passive: true })); const isObj = o => !!o && typeof o === 'object'; const axisK = (grow, shrink, size, d) => (grow ? (size + d) / size : shrink ? (size - d) / size : null); const isPlaying = v => !!v && !v.paused && !v.ended && v.readyState > 2; const escHTML = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const safeDecode = s => { try { return decodeURIComponent(s); } catch { return s; } }; const gmReq = o => new Promise(resolve => { const { method = 'GET', url, data, headers, type = 'text', timeout = 6000, signal, credentials, viaFetch } = o; let tm = 0; const onAbort = () => { try { req?.abort?.(); } catch (e) { dbg(e, 'gmReq.onAbort'); } try { ctrl?.abort(); } catch (e) { dbg(e, 'gmReq.onAbort'); } done(0, null, 'abort'); }; const done = (status, body, fail = '') => { clearTimeout(tm); signal?.removeEventListener('abort', onAbort); resolve({ status, body, fail }); }; let req, ctrl; if (signal?.aborted) return done(0, null, 'abort'); if (!viaFetch && typeof GM_xmlhttpRequest === 'function') { try { req = GM_xmlhttpRequest({ method, url, headers: headers || {}, timeout, ...(data !== undefined && { data }), ...(type !== 'text' && { responseType: type }), onload: r => done(r.status, type === 'text' ? (r.responseText || '') : r.response), onerror: () => done(0, null, 'error'), ontimeout: () => done(0, null, 'timeout') }); signal?.addEventListener('abort', onAbort, { once: true }); } catch { done(0, null, 'error'); } return; } ctrl = new AbortController(); let timedOut = false; if (timeout > 0) tm = setTimeout(() => { timedOut = true; ctrl.abort(); }, timeout); signal?.addEventListener('abort', onAbort, { once: true }); fetch(url, { method, cache: 'no-store', signal: ctrl.signal, ...(headers && { headers }), ...(data !== undefined && { body: data }), ...(credentials && { credentials }) }) .then(async r => done(r.status, type === 'arraybuffer' ? await r.arrayBuffer() : type === 'blob' ? await r.blob() : await r.text())) .catch(() => done(0, null, timedOut ? 'timeout' : 'error')); }); const bytesOf = r => (r.status >= 200 && r.status < 300 && r.body ? new Uint8Array(r.body) : null); const gmBytes = async (url, referer, gmFirst, range) => { const hasGm = typeof GM_xmlhttpRequest === 'function'; const rh = range ? { Range: `bytes=${range.o}-${range.o + range.n - 1}` } : null; const viaFetch = () => gmReq({ url, type: 'arraybuffer', timeout: 0, viaFetch: true, ...(rh && { headers: rh }) }); const viaGm = () => gmReq({ url, type: 'arraybuffer', timeout: 20000, headers: { Referer: referer || `${location.origin}/`, ...rh } }); const gone = r => r.status === 404 || r.status === 410; if (gmFirst) { if (!hasGm) return bytesOf(await viaFetch()); const g = await viaGm(); return bytesOf(g) || (gone(g) || g.fail === 'timeout' ? null : bytesOf(await viaFetch())); } const f = await viaFetch(); return bytesOf(f) || (gone(f) || !hasGm ? null : bytesOf(await viaGm())); }; const MINI_SHIFT_X = 168, MINI_SHIFT_Y = 5; const BM_HOST = /(^|\.)(bongacams2?\.com|bongocams\.com|webcamsluts\.ru|stripchat\.com|chaturbate\.com)$/; const isSafeUrl = u => { try { const x = new URL(u); return x.protocol === 'https:' && BM_HOST.test(x.hostname); } catch { return false; } }; const BC_RE = /bongacams|bongocams|webcamsluts/; const siteKey = h => BC_RE.test(h) ? 'bc' : /stripchat/.test(h) ? 'sc' : /chaturbate/.test(h) ? 'cb' : h; const urlSite = u => { try { return siteKey(new URL(u).hostname); } catch { return ''; } }; const SITE = siteKey(location.hostname), IS_BC = SITE === 'bc'; const toHttps = u => { u = typeof u === 'string' ? u : ''; if (u.startsWith('//')) u = `https:${u}`; return u.startsWith('https://') ? u : ''; }; const isStr = v => typeof v === 'string'; const clip = (v, n) => isStr(v) ? v.slice(0, n) : ''; const validBm = b => !!b && isStr(b.name) && isStr(b.url) && !!b.name.trim() && isSafeUrl(b.url); const cleanBm = b => { if (!validBm(b)) return null; const out = { name: clip(b.name, 100), url: normUrl(b.url), avatar: toHttps(b.avatar), note: clip(b.note, 500), g: clip(isStr(b.g) && b.g.trim(), 40), seen: Number.isFinite(b.seen) && b.seen > 0 ? b.seen : 0 }; return Object.assign(out, cleanBmExtras(b)); }; const cleanBmExtras = b => { const x = {}; if (b.pin === true) x.pin = true; if (Array.isArray(b.ptags)) { const keep = []; b.ptags.forEach((t, i) => { const c = clip(t, 40); if (c && keep.length < 30) keep.push([c, b.ptic?.[i]]); }); x.ptags = keep.map(p => p[0]); if (keep.some(p => p[1])) x.ptic = keep.map(p => (isStr(p[1]) && /^[a-z0-9_\/-]{1,60}$/i.test(p[1]) ? p[1] : '')); } if (Array.isArray(b.sch) && b.sch.length === 7) x.sch = b.sch.map(s => clip(s, 60)); return x; }; const HASHTAG_RE = /#[\p{L}\p{M}\p{N}_]+/giu; const noteHashTags = b => b.note?.match(HASHTAG_RE) || []; const bmTagMatch = (b, tag) => !tag || (tag[0] === '#' ? noteHashTags(b).some(t => t.toLowerCase() === tag) : (b.ptags || []).some(t => t.toLowerCase() === tag)); const countInto = (m, x) => { const k = x.toLowerCase(); m.set(k, (m.get(k) || 0) + 1); }; const RS_PULT_CSS = `.rs-pult{--pb:#0e0f12;--pc:#17191d;--pl:#2a2d33;--pt:#22252b;--pon:#fff;display:flex!important;flex-direction:column;font-family:Onest,system-ui,-apple-system,sans-serif;color:#fff;border:1px solid var(--pl)!important;border-radius:18px!important;box-shadow:0 14px 36px rgba(0,0,0,.6)!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;padding:0!important;overflow:hidden!important;width:460px;max-width:94vw;max-height:92vh;height:auto} @supports(color:color-mix(in srgb,red 10%,blue)){.rs-pult{--pb:color-mix(in srgb,var(--th) 7%,#0b0c0f);--pc:color-mix(in srgb,var(--th) 11%,#15171b);--pl:color-mix(in srgb,var(--th) 38%,#3a3e46);--pt:color-mix(in srgb,var(--th) 14%,#22252b)}} @supports(color:oklch(from red l c h)){.rs-pult{--pon:oklch(from var(--th) calc((0.7 - l) * 1000) 0 0 / 1)}} .rs-pult *{box-sizing:border-box} .rs-pult .bm-list{order:1;display:flex!important;flex-direction:column;gap:8px;padding:8px!important;flex:1 1 auto;min-height:0;overflow-y:auto;grid-template-columns:none!important;align-content:stretch;scrollbar-width:thin;scrollbar-color:var(--pl) transparent} .rs-pult .bm-hdr{order:2;position:static!important;display:flex!important;flex-wrap:nowrap!important;align-items:center;gap:8px;flex:none;padding:8px 8px calc(8px + env(safe-area-inset-bottom,0px))!important;margin:0!important;background:var(--pc)!important;border:0!important;border-top:1px solid var(--pl)!important} .rs-pult .bm-title{flex:1 1 auto;min-width:0;display:flex!important;align-items:center;gap:10px;font-size:15px;font-weight:500;color:#fff} .rs-pult .rs-ic{flex:none;width:40px;height:40px;border-radius:12px;background:var(--th);color:var(--pon);display:flex;align-items:center;justify-content:center} .rs-pult .rs-ic svg{display:block;margin:0!important;width:22px;height:22px} .rs-pult .bm-close{position:static!important;inset:auto!important;transform:none!important;flex:0 0 44px;width:44px;height:44px;margin:0!important;padding:0;border-radius:12px!important;background:var(--pb)!important;border:1px solid var(--pl)!important;color:#fff!important;display:flex;align-items:center;justify-content:center} .rs-pult .bm-close:hover{background:rgba(231,76,60,.9)!important;border-color:transparent!important} .rs-pult .rs-sum{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:14px;background:var(--pc);border:1px solid var(--pl);font-size:13px;font-weight:500;color:#fff} .rs-pult .rs-sum svg{flex:none;width:18px;height:18px;margin:0!important;color:var(--th);filter:brightness(1.4)} .rs-pult .rs-g{display:flex;flex-direction:column;gap:6px;padding:8px;border-radius:14px;background:rgba(0,0,0,.34)} .rs-pult .hd-sec{display:block!important;padding:2px 4px 0!important;margin:0;font-size:11px!important;font-weight:600!important;letter-spacing:.4px!important;text-transform:uppercase!important;color:#fff!important;opacity:.62} .rs-pult .rs-g .hd-sec:not(:first-child){margin-top:6px} .rs-pult .rs-qs{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} .rs-pult .rs-qs .bm-btn{display:flex!important;flex-direction:column;align-items:center;justify-content:center;gap:1px;min-height:78px;padding:10px 6px!important;margin:0!important;text-align:center!important;border-radius:12px!important;border:1px solid var(--pl)!important;background:var(--pt)!important;color:#fff!important;box-shadow:none!important;transition:background .12s,transform .12s} .rs-pult .rs-qs .bm-btn b{font-size:17px;font-weight:600} .rs-pult .rs-qs .bm-btn span{font-size:12px} .rs-pult .rs-qs .bm-btn small{font-size:11px;opacity:.7;margin-top:2px} .rs-pult .rs-qs .bm-btn:active,.rs-pult .rs-chips .bm-tbtn:active{transform:scale(.97)} .rs-pult .rs-qs .bm-btn.sel{background:var(--th)!important;border-color:transparent!important;color:var(--pon)!important} .rs-pult .rs-qs .bm-btn.sel small{opacity:.85} .rs-pult .rs-chips{display:flex;flex-wrap:wrap;gap:6px} .rs-pult .rs-chips .bm-tbtn{flex:1 1 auto;min-width:0;min-height:44px;margin:0!important;padding:0 12px!important;border-radius:12px!important;border:1px solid var(--pl)!important;background:var(--pt)!important;color:#fff!important;font-size:13px!important;font-weight:500;line-height:1.2;box-shadow:none!important;display:inline-flex;align-items:center;justify-content:center;text-align:center;transition:background .12s,transform .12s} .rs-pult .rs-chips .bm-tbtn.on{background:var(--th)!important;border-color:transparent!important;color:var(--pon)!important;font-weight:600} .rs-pult .rs-src .bm-tbtn{flex:1 1 0;padding:6px 10px!important} :host .bm-modal.rs-pult .rs-note,:host .bm-modal.rs-pult .rs-info{padding:0 6px;font-size:11px;line-height:1.5;color:#fff!important;opacity:.62} :host .bm-modal.rs-pult .rs-info{font-size:12px;opacity:.85} .rs-pult .rs-chips:empty{display:none} .rs-pult .bm-btn:focus-visible,.rs-pult .bm-tbtn:focus-visible,.rs-pult .bm-close:focus-visible{outline:2px solid var(--th);outline-offset:2px} @media (max-width:520px){.bm-overlay:has(.rs-pult){align-items:stretch}.bm-modal.rs-pult{height:100%!important;max-height:100%!important;width:100vw!important;max-width:100vw!important;border-radius:0!important;border-left:0!important;border-right:0!important;border-bottom:0!important;align-self:stretch!important}}`; const buildModalShell = (shadow, modalCls, bodyCls = 'bm-list') => { const overlay = mk('div', 'bm-overlay'); const modal = mk('div', modalCls); const hdr = mk('div', 'bm-hdr'); const title = mk('span', 'bm-title'); const closeBtn = mk('button', 'bm-close'); closeBtn.innerHTML = '×'; hdr.append(title, closeBtn); const body = mk('div', bodyCls); modal.append(hdr, body); overlay.appendChild(modal); shadow.appendChild(overlay); return { overlay, modal, title, closeBtn, body }; }; const bmTbtn = (html, active, onClick, title = '') => { const btn = mk('button', `bm-tbtn${active ? ' on' : ''}`); btn.innerHTML = html; btn.title = title; btn.onclick = onClick; return btn; }; const RESORT_SORTS = new Set(['online', 'last', 'viewers']); const stripBm = ({ _updateItemDOM, _topicExp, _lastImg, _lastData, _chk, _isUpdating, _avFix, _avK, _live, _mob, _pf, _sv, label, ...rest }) => rest; const BC_CONTAINERS = ['#stream-container', '#ResizablePane', '#EmbedSWFHolder', '.player-container', '.performer-video-container', '.chat-video-container', '.video-panel', '#video-panel-wrap']; const HIDE_DECL = 'display:none!important;opacity:0!important;pointer-events:none!important;height:0!important;margin:0!important;padding:0!important'; const HIDE_CATS = [ { id: 'hdr_m', plat: 'm', def: true, sel: ['.bc-header', '.bc-header-main', '.h_container', '#mainbar_container', '.join_button_container', '.js-chat_join_button', '[data-role="header"]'] }, { id: 'hdr_d', plat: 'd', def: false, sel: ['.js-page_header', '.js-header_bar'] }, { id: 'tabs', plat: 'm', def: true, sel: ['.live_tabs', '#mls_filter_result_container', '.bc_alternate_header_live_tabs'] }, { id: 'seotitle', plat: 'b', def: true, sel: ['.mls_title'] }, { id: 'chat_tips', plat: 'b', def: false, sel: ['.history .system_msg.tip_success', '.system_msg.tip_success'] }, { id: 'chat_topic', plat: 'b', def: false, sel: ['.history .system_msg:has(.bc_notice.__topic)', '.bc_notice.__topic'] }, { id: 'chat_boost', plat: 'b', def: false, sel: ['.history [class*="boost"]', '.history [class*="Boost"]'] }, { id: 'ftr', plat: 'b', def: true, sel: ['.bc-footer', '#footer_container', '.bcm_footer', '.bcm_socials', '.m_socials', 'footer.footer', '.footer_copy', '.footer_seo', '.footer_mobile_guide', '#jsx-ls_seo_custom_text', '#jsx-ls_footer_container'] }, { id: 'promo', plat: 'b', def: true, sel: ['.get_app_banner', '.js-get_app_banner', '.gab_wrp', '.banner_wrp', '.js-index_banner', '.sb_topmodel', '.free_tokens_panel', '#top_banner_above_chat', '.banner_top_chat', '.hb_item.__promo', '.js-free_tokens', '.hb_contest_item', '.holiday_discount_alert', '.js-top_banner', 'a[rel="buy_tokens"].holiday_discount_alert', '.stream_join_btn', '.join_button_container', '.bc_popalert.__give_away', '.bc_popalert.__member_bonus', '.stream_social_links', '.fancybox-overlay', '.fp_outer', '.fp_outer_overlay', '[class*="promo-banner"]', '[class*="age-gate"]', '[class*="age_gate"]', '[class*="cookie-banner"]', '[class*="notification-bar"]'] }, { id: 'lastview', plat: 'b', def: false, sel: ['.lst_bage.__last_viewed'] }, { id: 'joinpop', plat: 'b', def: false, sel: ['#member_join_popup', '.member_join_popup', '.fp_outer', '.fp_outer_overlay', '.fp_inner_wrap', '*:has(> .fp_inner_wrap)'] }, { id: 'tips', plat: 'b', def: true, sel: ['#firstChatTooltipContainer', '.chat_bar_alert_container', '.chat_alert_container', '#chat_prompt_container', '#alerts_above_chat', '.js-bottom_alerts_area', '#bFontSizeTipHolder'] }, { id: 'modelchat', plat: 'b', def: true, sel: ['[data-bc-mpop]'] }, { id: 'games', plat: 'm', def: true, sel: ['#games_button'] }, { id: 'popups', plat: 'm', def: false, sel: ['#popupChatSelection', '#popupGroup', '#popupVoyeur', '#gamecontrol_popup_container', '.ui-popup-container', '.cam2cam_modal'] }, { id: 'actions', plat: 'b', def: false, sel: ['#chat_actions_bar', '.bcm_chat_footer', '#private_btn_cell', '.cf_cell.__private', 'a.cf_item.__tip', 'button.cf_item.__tip_menu', '.private_btn_wrp', '#bPaidButtonTopRow', '#bPaidButtonGroup', '.paid_button_group', '#bGotoGroup', '#bGotoPrivate', '#bGotoVoyeur', '#bTip'] }, { id: 'balance', plat: 'd', def: false, sel: ['.memberBalance', '.memberNavigation'] }, { id: 'chathdr', plat: 'b', def: false, sel: ['#id-chat-header', '.bc_chat_header'] }, { id: 'tipking', plat: 'b', def: false, sel: ['.tip_king_wrp', '.bc_new_tip_modal'] }, { id: 'below', plat: 'd', def: false, sel: ['.stream-controls'] }, { id: 'wm', plat: 'd', def: false, sel: ['#layover-logo', '.layover-logo'] }, { id: 'age18', plat: 'b', def: true, sel: ['#popup_18_plus', '#popup_18_plus-screen', '#popup_18_plus-popup'] } ]; HIDE_CATS.forEach(c => { c.site = 'bc'; }); const SC_CATS = [ { id: 'sc_hdr', def: true, sel: ['.header-top-wrapper'] }, { id: 'sc_sub', def: false, sel: ['.view-cam-header-sub'] }, { id: 'sc_promo', def: true, sel: ['[class*="SidebarBanners"]', '.tokens-giveaway-banner', '.tokens-giveaway-banner-container', '.player-join-button', '.player-controls-user__join-btn'] }, { id: 'sc_cookie', def: true, sel: ['[class*="CookieReminder"]', '.cookies-banner', '[data-testid="CookiesReminder"]'] }, { id: 'sc_footer', def: true, sel: ['#MAIN_FOOTER_ID', '[class*="footer-container"]', '[class*="Footer__inner"]'] }, { id: 'sc_widgets', def: true, sel: ['.plugin-panel', '.viewcam-widget-panel'] }, { id: 'sc_goal', def: false, sel: ['.view-cam-info-goal', '.epic-goal-progress__wrap'] }, { id: 'sc_topic', def: false, sel: ['.view-cam-info-topic'] }, { id: 'sc_king', def: false, sel: ['[data-testid="king-wrapper-is-king"]', '[data-testid="view-cam-info-king-content"]'] }, { id: 'sc_wm', def: false, sel: ['[class*="Watermark"]', '.watermark'] }, { id: 'sc_about', def: false, sel: ['[data-testid="viewcam-model-sections"]'] } ]; const CB_CATS = [ { id: 'cb_hdr', def: false, sel: ['.MobileHeader', '#header-links'] }, { id: 'cb_alerts', def: true, sel: ['.LanguageMismatchAlert', '.dismissible_notice', '.MobileDismissibleMessages', '#ie-support-notice'] }, { id: 'cb_footer', def: true, sel: ['#footer', '#mobile-social-media-icons'] }, { id: 'cb_sort', def: false, sel: ['.MobileSort', '#mobile-sort-container', '#mobile-sort-form-container'] }, { id: 'cb_tags', def: false, sel: ['.TopTagsBanner'] }, { id: 'cb_tips', def: false, sel: ['.roomNotice.isTip'] }, { id: 'cb_status', def: false, sel: ['.roomStatusNotifier'] }, { id: 'cb_actions', def: false, sel: ['.sendTipButton', '.tipForm'] }, { id: 'cb_info', def: false, sel: ['.HeaderRoomInfo'] } ]; SC_CATS.forEach(c => HIDE_CATS.push({ ...c, site: 'sc', plat: 'b' })); CB_CATS.forEach(c => HIDE_CATS.push({ ...c, site: 'cb', plat: 'b' })); const HIDE_TEXT = { hdr_m: ['Верх сайта', 'Логотип, меню, кнопки входа', 'Site top bar', 'Logo, menu, login buttons'], hdr_d: ['Шапка сайта (ПК)', 'Верхняя панель на компьютере', 'Site header (PC)', 'Top bar on desktop'], seotitle:['Заголовок-реклама', 'Строка «Безкоштовний секс в онлайн порно-чаті» над списком', 'Ad headline', 'The "Free sex in online porn chat" line above the list'], chat_tips:['Чаевые в чате', 'Сообщения «подарил токены», «Меню чаевых», тип-меню', 'Tips in chat', '\"Tipped N tokens\" and tip-menu messages'], chat_topic:['Тема комнаты', 'Длинное описание «Тема: …» в начале чата', 'Room topic', 'Long \"Topic: …\" block at the top of chat'], chat_boost:['Бусты в чате', 'Плашки «поднял модель на N секунд» (FAN BOOSTED)', 'Boosts in chat', '\"Boosted the model\" cards'], tabs: ['Вкладки и сортировка', 'Девушки/пары, фильтры, значок сортировки', 'Tabs & sorting', 'Category tabs, filters, sort icon'], ftr: ['Низ страницы', 'Подвал, соцсети, SEO-текст', 'Page bottom', 'Footer, socials, SEO text'], promo: ['Реклама и баннеры', 'Баннеры, промо, плашка «Присоединяйся» на плеере, приложение', 'Ads & banners', 'Banners, promos, "Join me" strip on the player, get-the-app'], lastview:['Плашка «Продолжить просмотр»', 'Кнопка на превью последней модели', '"Continue watching" badge', 'Button on the last viewed thumbnail'], joinpop: ['Окно регистрации', 'Всплывающая форма «Создайте аккаунт»', 'Sign-up popup', '"Create an account" popup'], tips: ['Подсказки в чате', 'Всплывающие подсказки и уведомления', 'Chat hints', 'Tooltips and alerts'], modelchat:['Всплывающий чат модели', 'Розовое окошко «Привет! Любишь виртуальный секс?» внизу', 'Model popup chat', 'Pink pop-up with the model\'s auto-greeting at the bottom'], games: ['Кнопка «Игры»', 'Иконка геймпада в красной панели', '"Games" button', 'Gamepad icon in the red bar'], popups: ['Всплывающие окна', 'Выбор чата, группа, подглядывание', 'Popups', 'Chat choice, group, voyeur'], actions: ['Кнопки «Приват / Чаевые»','Синие (на ПК зелёные) кнопки под красной панелью', '"Private / Tips" buttons','Blue (green on PC) buttons under the red bar'], balance: ['Баланс и меню (ПК)', 'Баланс и навигация на компьютере', 'Balance & menu (PC)', 'Balance and navigation on desktop'], chathdr: ['Плашка «Чат с …»', 'Строка «Бесплатный чат с …» и король чаевых', 'Title bar "Chat with …"','"Free chat with …" strip and tip king'], tipking: ['Король чаевых', 'Строка короля и окна чаевых', 'Tip king', 'Tip king strip and tip popups'], below: ['Панель под плеером (ПК)', 'Блок кнопок под видео', 'Under-player bar (PC)', 'Button row under the video'], wm: ['Водяной знак (ПК)', 'Логотип поверх видео', 'Watermark (PC)', 'Logo over the video'], age18: ['Окно «Старше 18»', 'Заставка «Заходи ТОЛЬКО если ты уже старше 18»', '18+ notice', 'The "18+ only" entry screen'] }; Object.assign(HIDE_TEXT, { sc_hdr: ['Верх сайта', 'Шапка с логотипом, поиском и кнопками входа', 'Site top bar', 'Header with logo, search and login buttons'], sc_sub: ['Строка модели', 'Аватар, подписка и меню над плеером', 'Model bar', 'Avatar, follow and menu row above the player'], sc_promo: ['Реклама и баннеры', 'Розыгрыши токенов, кнопка «Присоединиться» на плеере', 'Ads & banners', 'Token giveaways, Join button on the player'], sc_cookie: ['Плашка cookies', 'Уведомление о cookie внизу', 'Cookie notice', 'Cookie reminder at the bottom'], sc_footer: ['Низ страницы', 'Подвал и юридический текст', 'Page bottom', 'Footer and legal text'], sc_widgets:['Виджеты на плеере', 'Опросы и реакции поверх видео', 'Player widgets', 'Polls and reactions over the video'], sc_goal: ['Цель (гол)', 'Полоса прогресса цели', 'Goal bar', 'Goal progress bar'], sc_topic: ['Тема комнаты', 'Текст темы над и внутри плеера', 'Room topic', 'Topic text over and inside the player'], sc_king: ['Король чаевых', 'Плашка короля чаевых', 'Tip king', 'Tip king strip'], sc_wm: ['Водяной знак', 'Логотип поверх видео', 'Watermark', 'Logo over the video'], sc_about: ['Блоки под плеером', 'Обо мне, расписание, похожие модели', 'Below the player', 'About, schedule, similar models'], cb_hdr: ['Верх сайта', 'Шапка с логотипом и меню', 'Site top bar', 'Header with logo and menu'], cb_alerts: ['Уведомления', 'Предупреждение о языке и закрываемые заметки', 'Alerts', 'Language alert and dismissible notes'], cb_footer: ['Низ страницы', 'Подвал и соцсети', 'Page bottom', 'Footer and socials'], cb_sort: ['Сортировка', 'Панель сортировки в мобильной версии', 'Sorting', 'Mobile sorting panel'], cb_tags: ['Полоса тегов', 'Популярные теги над списком', 'Tag strip', 'Top tags above the list'], cb_tips: ['Чаевые в чате', 'Сообщения о токенах и чаевых', 'Tips in chat', 'Tip and token messages'], cb_status: ['Статус комнаты', 'Плашка про оффлайн и приват с подсказками', 'Room status', 'Offline and private notifier'], cb_actions:['Кнопка чаевых', 'Кнопка и форма отправки токенов', 'Tip button', 'Send-tip button and form'], cb_info: ['Название комнаты', 'Имя и счётчик зрителей в шапке', 'Room info', 'Name and viewers in the header'] }); HIDE_CATS.forEach(c => { const t = HIDE_TEXT[c.id]; if (t) { c.ru = t[0]; c.dru = t[1]; c.en = t[2]; c.den = t[3]; } }); const HIDE_PRESETS = { min: ['promo', 'joinpop', 'tips', 'modelchat', 'popups', 'seotitle', 'sc_promo', 'sc_cookie', 'sc_widgets', 'cb_alerts'], clean: HIDE_CATS.filter(c => c.site === 'bc' ? !/^chat_/.test(c.id) : c.def).map(c => c.id) }; const catOn = (c, cfg) => cfg[c.id] === undefined ? c.def : !!cfg[c.id]; const detectPlatform = () => document.body?.classList.contains('ui-mobile-viewport') ? 'm' : 'd'; const customAll = () => { const o = Store.get('bc_hide_custom', {}); return (o && typeof o === 'object' && !Array.isArray(o)) ? o : {}; }; const getCustom = () => { const a = customAll()[siteKey(location.hostname)]; return Array.isArray(a) ? a.filter(x => typeof x === 'string') : []; }; const setCustom = list => { const o = customAll(); o[siteKey(location.hostname)] = list; Store.set('bc_hide_custom', o); }; const addCustom = sel => { const l = getCustom(); if (!l.includes(sel)) { l.push(sel); setCustom(l); } }; const removeCustom = sel => setCustom(getCustom().filter(x => x !== sel)); const buildHideCSS = skipId => { const out = []; { const cfg = Store.get('bc_hide_cfg', {}); const hdr = HIDE_CATS.find(x => x.id === 'hdr_m'); if (IS_BC && hdr && !catOn(hdr, cfg)) out.push('.bc-header,.bc_header.bc-header-main,.bc-header__fixed,.bc-header.hidden,.bc-header__fixed.hidden{display:flex!important;visibility:visible!important;opacity:1!important;transform:none!important;pointer-events:auto!important;position:fixed!important;top:0!important;z-index:1000!important}.bc-header.hidden:before,.bc-header.hidden .ui-title,.bc-header.hidden .lbtn,.bc-header.hidden .rbtn,.bc-header.hidden .ui-title.logo{transform:none!important}'); const scope = { m: 'body.ui-mobile-viewport ', d: 'body:not(.ui-mobile-viewport) ' }; HIDE_CATS.forEach(c => { if (c.site !== SITE || c.id === skipId || !catOn(c, cfg)) return; const pre = c.site === 'bc' ? (scope[c.plat] || '') : ''; out.push(`${c.sel.map(x => pre + x).join(',')}{${HIDE_DECL}}`); }); } getCustom().forEach(x => out.push(`${x}{${HIDE_DECL}}`)); return out.join('\n'); }; const applyHideCSS = skipId => { let el = document.getElementById('bc-unchained-hide-css'); if (!el) { el = document.createElement('style'); el.id = 'bc-unchained-hide-css'; (document.head || document.documentElement).appendChild(el); } el.textContent = buildHideCSS(typeof skipId === 'string' ? skipId : undefined); }; const ensureHideCSS = () => { if (!document.getElementById('bc-unchained-hide-css')) applyHideCSS(); }; const cssEsc = x => (window.CSS && CSS.escape) ? CSS.escape(x) : String(x).replace(/[^\w-]/g, '\\$&'); const stableCls = c => c.length < 40 && !/^(ui-|__|bcu-|bc-(?:theater|thtr|unlock))/.test(c) && !/\d{3,}/.test(c) && !/^(active|open|opened|hidden|visible|selected|current|hover|focus|loaded|loading|playing|disabled|on|off|hd)$/i.test(c); const selUniq = x => { try { return document.querySelectorAll(x).length === 1; } catch { return false; } }; const pathSelector = el => { const parts = []; for (let cur = el; cur && cur.nodeType === 1 && cur !== document.documentElement; cur = cur.parentElement) { if (cur.id && selUniq(`#${cssEsc(cur.id)}`)) { parts.unshift(`#${cssEsc(cur.id)}`); break; } const par = cur.parentElement; const idx = par ? [...par.children].indexOf(cur) + 1 : 1; parts.unshift(`${cur.tagName.toLowerCase()}:nth-child(${idx})`); if (cur === document.body) break; } return parts.join(' > '); }; const isPickable = n => n?.nodeType === 1 && n !== document.body && n !== document.documentElement; const idSelector = c => { if (!c.id || /\d{3,}/.test(c.id)) return ''; const s = `#${cssEsc(c.id)}`; return selUniq(s) ? s : ''; }; const classSelector = c => c.tagName.toLowerCase() + [...c.classList].filter(stableCls).slice(0, 2).map(x => `.${cssEsc(x)}`).join(''); const buildSelector = el => { const parts = []; let cur = el; for (let i = 0; i < 6 && isPickable(cur); i++, cur = cur.parentElement) { const id = idSelector(cur); if (id) { parts.unshift(id); const full = parts.join(' > '); return selUniq(full) ? full : pathSelector(el); } parts.unshift(classSelector(cur)); const sel = parts.join(' > '); if (selUniq(sel)) return sel; } return pathSelector(el); }; const BC_CSS = `a.htlp_icon_wrp.bg-head{display:none!important}#header_login{display:block!important;opacity:1!important;pointer-events:auto!important;visibility:visible!important}#header_login .join_btn,#header_login .jbt_inner{opacity:1!important;pointer-events:auto!important;visibility:visible!important}.performer-frame,.blurred-performer-frame{pointer-events:none!important}#video-panel-wrap,#stream-container,[class*="video-container"],[class*="player-wrap"]{pointer-events:auto!important;touch-action:manipulation}.chat_panel_wrap{height:100%!important;flex:1}body:not(.page_index):not(.__page_main),body:not(.page_index):not(.__page_main) .ui-page,body:not(.page_index):not(.__page_main) .js-page,body:not(.page_index):not(.__page_main) .bc-content{padding-top:0!important;margin-top:0!important}body:not(.__page_main) .bc_alternate_header_live_tabs,body:not(.__page_main) .mls_hash_tag,body:not(.__page_main) .__fixed_top{position:static!important;margin:0!important}.bc_sticky_bar{position:fixed!important;top:var(--bc-sticky-y,80px)!important;left:var(--bc-sticky-x,50%)!important;transform:var(--bc-sticky-tf,translateX(-50%))!important;margin:0!important;bottom:auto!important;z-index:9990!important;pointer-events:none!important}.js-filter_button{pointer-events:auto!important;display:flex;justify-content:center}#chat_bar_v2_container{display:block!important;opacity:1!important;pointer-events:auto!important;z-index:2147483605!important}#chat_bar_v2,#chatProfileButton,#chatBarChatButton,#switch_between_chat{display:flex!important;opacity:1!important;pointer-events:auto!important;z-index:2147483605!important}@media (min-width:1024px){*:fullscreen video,*:-webkit-full-screen video,video:fullscreen,video:-webkit-full-screen{object-fit:contain!important;width:100%!important;height:100%!important;max-width:100vw!important;max-height:100vh!important;margin:auto!important}}`; const firstSegment = () => location.pathname.split('/').filter(Boolean)[0] || ''; const slugName = (reserved, valid) => () => { const f = firstSegment(); return f && !reserved.test(f) && valid.test(f) ? f : ''; }; const BC_SYS = /^(?:ru|uk|ua|en|es|de|fr|pt|it|pl|tr|nl|cs|ro|hu|bg|el|ja|zh|ko|he|ar|hi|id|vi|th|tags?|categor(?:y|ies)|free-cams|all|search|girls|couples?|male|men|guys|female|trans|transsexual|new|top|models|favou?rites|live|cams|blog|help|support|settings|terms|privacy|faq|contacts|login|signup|register|apps|mobile|profile)$/i; const bcDisplayName = () => { const d = Helpers.getModelSecretData(); return d?.chatHost?.displayName || document.querySelector('#bChatRoomTitle, .chat-header-inner h1 a')?.textContent?.trim() || ''; }; const bcFallbackName = () => { const d = Helpers.getModelSecretData(); return bcDisplayName() || d?.chatHost?.username || ''; }; const SC_SYS = /^(?:girls|men|guys|couples|trans|tags?|favorites|favourites|login|signup|search|blog|cams|explore|top|api|help|support|terms|privacy|models|profile|settings|videos|photos|pricing|studios|category|categories|new|vr|contest|announcement|user|guests|latest|about|contact|dmca|2257|tokens|messages|notifications|affiliates|shop|rewards|community)$/i; const CB_SYS = /^(?:b|tags?|.+-cams|accounts|auth|tipping|supporter|affiliates|api|contest|apps|security|terms|privacy|dmca|support|billing|photo_videos|store|login|signup|register|search|explore|hosting|location|followed|follow|female|male|couple|trans|featured|2257|tokens|purchase|help|about|press|contact|sitemap|faq|studios|business|broadcast|messages|notifications|settings|cams|chatbot)$/i; const FS_CSS = '@media (min-width:1024px){*:fullscreen video,*:-webkit-full-screen video,video:fullscreen,video:-webkit-full-screen{object-fit:contain!important;width:100%!important;height:100%!important;max-width:100vw!important;max-height:100vh!important;margin:auto!important}}'; const currentSite = (() => { const hostname = window.location.hostname; const configs = { bongacams: { match: BC_RE, videoSelectors: BC_CONTAINERS.map(s => `${s} video`).concat('video'), containerSelectors: BC_CONTAINERS, getName: () => { const [first = '', second = ''] = location.pathname.split('/').filter(Boolean); const raw = !BC_SYS.test(first) ? first : /^profile$/i.test(first) ? second : ''; return bcDisplayName() || safeDecode(raw) || bcFallbackName(); }, css: BC_CSS }, stripchat: { match: /stripchat|strpst/, videoSelectors: ['video.video-element', '.player video', '.web-rtc-player video', 'video'], containerSelectors: ['.player-wrapper', '[class*="ViewCamPlayer"]', '.web-rtc-player-wrapper', '.video-element-wrapper'], getName: slugName(SC_SYS, /^[\w.-]+$/), css: FS_CSS }, chaturbate: { match: /chaturbate/, videoSelectors: ['#chat-player_html5_api', '#VideoPanel video', '.videoPlayerDiv video', '.video-js video', 'video'], containerSelectors: ['#TheaterModePlayer', '#VideoPanel', '.videoPlayerDiv', '#chat-player', '.video-js', '#video_container'], getName: slugName(CB_SYS, /^\w+$/), css: FS_CSS } }; return Object.values(configs).find(c => c.match.test(hostname)) || configs.bongacams; })(); const LANGS = ['RU', 'EN', 'UK', 'ES', 'DE', 'FR', 'PT', 'IT', 'PL', 'TR', 'NL', 'CS', 'RO', 'HU', 'BG', 'EL', 'JA', 'ZH', 'KO', 'HE', 'AR', 'HI', 'ID', 'VI', 'TH']; const RTL_LANGS = ['HE', 'AR']; const LANG_NAMES = { RU: 'Русский', EN: 'English', UK: 'Українська', ES: 'Español', DE: 'Deutsch', FR: 'Français', PT: 'Português', IT: 'Italiano', PL: 'Polski', TR: 'Türkçe', NL: 'Nederlands', CS: 'Čeština', RO: 'Română', HU: 'Magyar', BG: 'Български', EL: 'Ελληνικά', JA: '日本語', ZH: '中文', KO: '한국어', HE: 'עברית', AR: 'العربية', HI: 'हिन्दी', ID: 'Bahasa Indonesia', VI: 'Tiếng Việt', TH: 'ไทย' }; const detectLang = () => { let list = []; try { list = [...(navigator.languages?.length ? navigator.languages : [navigator.language]), document.documentElement?.lang]; } catch (e) { dbg(e, 'detectLang'); } for (const c of list) { const k = String(c || '').slice(0, 2).toUpperCase().replace('IW', 'HE'); if (LANGS.includes(k)) return k; } return 'EN'; }; const savedLang = Store.get('bc_lang', null); const langAuto0 = Store.get('bc_lang_auto', savedLang == null) === true; const DEFAULT_THEME = 'rgba(220, 20, 60, 0.9)'; const AppState = { bgPlay: Store.get('bc_bg', true), isMinimized: Store.get('bc_min', false), isHidden: Store.get('bc_hid', false), scale: parseFloat(Store.get('bc_scale', 1)), posX: Store.get('bc_x', null), posY: Store.get('bc_y', null), hasPos: Store.get('bc_pos', false), theme: Store.get('bc_theme', DEFAULT_THEME), langAuto: langAuto0, lang: langAuto0 ? detectLang() : (LANGS.includes(savedLang) ? savedLang : 'RU'), gmute: Store.get('bc_gmute', false) === true, bookmarks: Store.get('bc_bookmarks', []).filter(isObj), dark: Store.get('bc_dark', false) === true }; const FavNav = { get on() { return Store.get('bc_favnav', false) === true; }, set(v) { Store.set('bc_favnav', !!v); this.apply(); }, apply() { try { this.hook && this.hook(this.on); } catch (e) { dbg(e, 'apply'); } } }; const bmTomb = new Set(); const bmPersisted = new Set(); const BM_BAK = 'bc_bookmarks_bak'; let bmSaveFailed = false, bmDirty = false, bmNeedSave = false, bmRetryT = 0, bmRetries = 0, bmPersistedInit = false, bmBakAt = 0, bmBakLen = -1; const bmRuntimeKey = k => k[0] === '_' || k === 'label'; const bmEnsurePersisted = () => { if (bmPersistedInit) return; bmPersistedInit = true; try { const a = Store.get('bc_bookmarks', []); if (Array.isArray(a)) for (const r of a) if (validBm(r)) bmPersisted.add(bmKey(r.url)); } catch (e) { dbg(e, 'bmEnsurePersisted'); } }; const saveBookmarks = () => { try { const have = new Set(AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)).map(b => bmKey(b.url))); const stored = Store.get('bc_bookmarks', []); if (Array.isArray(stored)) for (const r of stored) { if (!validBm(r)) continue; const k = bmKey(r.url); if (have.has(k) || bmTomb.has(k)) continue; have.add(k); AppState.bookmarks.push({ ...stripBm(r) }); } } catch (e) { dbg(e, 'saveBookmarks'); } const out = AppState.bookmarks.filter(isObj).map(stripBm); bmSaveFailed = !Store.set('bc_bookmarks', out); if (!bmSaveFailed) { bmTomb.clear(); bmDirty = false; bmRetries = 0; bmPersistedInit = true; bmPersisted.clear(); for (const r of out) if (isStr(r.url)) bmPersisted.add(bmKey(r.url)); if (out.length && (out.length !== bmBakLen || Date.now() - bmBakAt > 60000)) { Store.set(BM_BAK, out); bmBakLen = out.length; bmBakAt = Date.now(); } } else if (!bmRetryT && bmRetries < 3) { bmRetries++; bmRetryT = setTimeout(() => { bmRetryT = 0; saveBookmarks(); }, 2500); } }; const incomingBookmarks = arr => { const m = new Map(); for (const r of arr) if (validBm(r)) m.set(bmKey(r.url), r); return m; }; const BM_LIVE = ['status', 'viewers', 'frame', 'topic']; const mergeInto = (b, src, skip) => { for (const f of Object.keys(b)) if (!bmRuntimeKey(f) && !BM_LIVE.includes(f) && !(f in src) && !skip.includes(f)) delete b[f]; for (const f of Object.keys(src)) if (!skip.includes(f)) b[f] = src[f]; }; const mergeLocalBookmark = (b, incoming, seen, skip) => { if (!isStr(b.url)) return true; const k = bmKey(b.url), r = incoming.get(k); if (seen.has(k)) return false; if (!r && !validBm(b)) return false; if (!r && bmPersisted.has(k) && !bmSaveFailed) return false; seen.add(k); if (r) mergeInto(b, stripBm(r), skip); else bmNeedSave = true; return true; }; const mergeRemoteBookmarks = (arr, skip = []) => { if (!Array.isArray(arr)) return false; bmEnsurePersisted(); bmNeedSave = false; const incoming = incomingBookmarks(arr), out = [], seen = new Set(); for (const b of AppState.bookmarks) { if (isObj(b) && mergeLocalBookmark(b, incoming, seen, skip)) out.push(b); } incoming.forEach((r, k) => { bmPersisted.add(k); if (!seen.has(k)) out.push({ ...stripBm(r) }); }); AppState.bookmarks = out; if (bmNeedSave) { bmNeedSave = false; bmDirty = true; setTimeout(saveBookmarks, 0); } return true; }; const syncBookmarks = () => { try { const a = Store.get('bc_bookmarks', null); if (Array.isArray(a)) mergeRemoteBookmarks(a); } catch (e) { dbg(e, 'syncBookmarks'); } }; const bmDropHooks = []; const dropBookmark = b => { const k = isStr(b.url) ? bmKey(b.url) : null; if (k) bmTomb.add(k); AppState.bookmarks = AppState.bookmarks.filter(x => x !== b && !(k && isObj(x) && isStr(x.url) && bmKey(x.url) === k)); saveBookmarks(); if (!AppState.bookmarks.length) { Store.set(BM_BAK, []); bmBakLen = -1; } if (k) for (const h of bmDropHooks) { try { h(k); } catch (e) { dbg(e, 'dropBookmark'); } } }; try { if (!AppState.bookmarks.length) { const bak = Store.get(BM_BAK, []); const good = Array.isArray(bak) ? bak.filter(validBm).map(stripBm) : []; if (good.length) { AppState.bookmarks = good; bmDirty = true; setTimeout(() => saveBookmarks(), 1500); } } } catch (e) { dbg(e, 'top'); } const flushBookmarks = () => { if (bmDirty) { try { saveBookmarks(); } catch (e) { dbg(e, 'flushBookmarks'); } } }; window.addEventListener('pagehide', flushBookmarks, { capture: true }); document.addEventListener('visibilitychange', () => { let hid; try { hid = typeof realHidden === 'function' ? realHidden() : document.hidden; } catch { hid = document.hidden; } if (hid) flushBookmarks(); }, true); const setGMute = v => { AppState.gmute = !!v; Store.set('bc_gmute', !!v); }; const sameBm = (b, name) => String(b.name).toLowerCase() === String(name).toLowerCase() && urlSite(b.url) === SITE; const mergeBookmarks = arr => { const seen = new Set(AppState.bookmarks.map(b => bmKey(b.url))); let added = 0; for (const raw of arr) { const b = cleanBm(raw); if (!b) continue; const k = bmKey(b.url); if (seen.has(k)) continue; seen.add(k); bmTomb.delete(k); AppState.bookmarks.push(b); added++; } saveBookmarks(); return added; }; const DARK_PLAYER = '#TheaterModePlayer,.video-js:not(#TheaterModePlayer .video-js,#basePlayer .video-js)'; const DARK_PLAYER_MEDIA = 'video,img,picture,canvas,iframe,[style*="background-image"]'; const DARK_FB_CSS = `html.bc-dark-fb{filter:invert(1) hue-rotate(180deg)!important;background:#fff} html.bc-dark-fb :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}){filter:invert(1) hue-rotate(180deg)!important} html.bc-dark-fb :is(${DARK_PLAYER}) :is(${DARK_PLAYER_MEDIA}){filter:none!important} html.bc-dark-fb:not(.bc-theater-html):not(.bc-fs-on) .RoomCardThumbnail::after,html.bc-dark-fb:not(.bc-theater-html):not(.bc-fs-on) .RoomCardThumbnail__bottomBar{filter:invert(1) hue-rotate(180deg)!important} html.bc-dark-fb.bc-theater-html,html.bc-dark-fb.bc-theater-html :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}){filter:none!important} html.bc-dark-fb.bc-fs-on,html.bc-dark-fb.bc-fs-on *,html.bc-dark-fb.bc-fs-on :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}){filter:none!important} html.bc-dark-fb :fullscreen,html.bc-dark-fb :fullscreen *,html.bc-dark-fb :fullscreen :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}),html.bc-dark-fb :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}):fullscreen{filter:none!important}`; const DARK_FB_CSS_WK = `html.bc-dark-fb :-webkit-full-screen,html.bc-dark-fb :-webkit-full-screen *,html.bc-dark-fb :-webkit-full-screen :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}),html.bc-dark-fb :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}):-webkit-full-screen{filter:none!important}`; const syncFsDark = () => { try { document.documentElement.classList.toggle('bc-fs-on', !!(document.fullscreenElement || document.webkitFullscreenElement)); } catch (e) { dbg(e, 'syncFsDark'); } }; document.addEventListener('fullscreenchange', syncFsDark, true); document.addEventListener('webkitfullscreenchange', syncFsDark, true); const DARK_M_CSS = () => `html.bc-dark-m{color-scheme:dark;background:#121216} html^body,html^.ui-page,html^.ui-body-c,html^.ui-content,html^.ui-panel-content-wrap,html^.bc-content{background:#121216!important;color:#dedede;text-shadow:none} html^#chat_bar_v2_container,html^.inputContainer,html^.video-panel-wrap{background:#18181c!important} html^.history,html^.history p.msg_wrp,html^.history .msg,html^.history dfn:after{color:#dedede} html^.history p:not(.model) .author_name{color:#e8e8ec} html^.np_title,html^.nt_message_title,html^.bc_notice.__topic{color:#ee5b78} ^.cp_purchase_popup{--bc-thm-purchasePopup-bg:#121213;--bc-thm-purchasePopup-accent-color:#e17085;--bc-thm-purchasePopup-avatar-bg:#212124;--bc-thm-purchasePopup-desc-divider-bg:#212124;--bc-thm-purchasePopup-title-color:#bcbcbc;--bc-thm-purchasePopup-desc-color:#d4d4d4} ^.bc_confirm_popup_wrp{--bc-thm-confirmPopup_error-color:#f65a61;--bc-thm-confirmPopup_action_column-confirmBtn_decline-bg:#252527;--bc-thm-confirmPopup_action_column-confirmBtn_decline-bg-active:#323235;--bc-thm-confirmPopup_fanBoostWhat-bg:#121213;--bc-thm-confirmPopup_fanBoostWhat-color:#d8d8d8;--bc-thm-confirmPopup_fanBoostWhat-accent-color:#d67b87;--bc-thm-confirmPopup_fanBoostWhat-benefits-bg:#151517;--bc-thm-confirmPopup_fanBoostWhat-statuses-item-borderColor:#3b3b3f} ^.bc_confirm_popup.__confirmed_light{--bc-thm-confirmPopup_confirmLight-bg:#151516;--bc-thm-confirmPopup_confirmLight-color:#d8d8d8;--confirmPopup_profile_media-confirm-title-color:#e17085;--confirmPopup_profile_media-confirmBtn_decline-color:#cecece} ^.bc_confirm_popup_wrp.__vertical_orientation{--confirmPopup_vertical_orientation-color:#d8d8d8} ^.bc_confirm_popup.__qoq_contest{--bc-thm-confirmPopup_qoqContest-color:#f5f5f5} ^.bc_confirm_popup.__gift_preview{--confirmPopup_giftPreview-body-bg:#121213} ^.bc_confirm_popup.__private_close{--bcm-privateClose-title-color:#e17085} ^.mbc_btn{--bcm-btn-color:#cecece;--bcm-btn-bg:#252527;--bcm-btn-bg-active:#1d1d1f;--bcm-btn-bg-hover:#1b1b1d;--bcm-btn_secondary-color:#cecece;--bcm-btn_secondary-bg:#1c1c1e;--bcm-btn_secondary-bg-active:#323235;--bcm-btn_secondary-bg-hover:#1b1b1d;--bcm-btn_outline-borderColor:#3e3e43} ^.bc_static_page{--bcm-static-page-color:#d8d8d8;--bcm-static-page-striped-content-div-even-text-shadow-color:rgba(0,0,0,0);--bcm-static-page-striped-content-div-even-bg-color:#1a1a1c;--bcm-static-page-header-text-shadow-color:rgba(0,0,0,0);--bcm-static-page-header-bg-color:#1a1a1c} ^#mainbar{--bcm-mainbar-btn-color:#d8d8d8;--bcm-mainbar-btn-bg-active:#19191b;--bcm-mainbar-menuItemList-subItem-color:#e17085} ^#mainbar .holiday_discount{--bcm-sidebar-holidayDiscount-accent-color:#e17085} ^.bc_alternate_header_live_tabs{--alternateHeaderLiveTabs-base-color:#d8d8d8;--alternateHeaderLiveTabs-bg:#18181a;--alternateHeaderLiveTabs-scrollShadow-bg:linear-gradient(270deg, #18181a 0%, #f2f2f200 100%)} ^.bc_skeleton_item{--bcm-skeleton-item-bg-color:#18181a} ^.bc_skeleton_item:before{--bcm-skeleton-item-pseudo-bg-color:#121213} ^.bcm_alphabet_filter{--bcm-alphabetFilter-base-accent-color:#e17085;--bcm-alphabetFilter-base-borderColor:#404044;--bcm-alphabetFilter-dropdown-bg:#121213;--bcm-alphabetFilter-dropdown-item-color:#d8d8d8;--bcm-alphabetFilter-select-bg:#121213;--bcm-alphabetFilter-select-caption-color:#d8d8d8} ^.bcm_pagination{--bcm-pagination-item-color:#f3f3f3;--bcm-pagination-item-bg-color:#18181a;--bcm-pagination-item-bg-color-active:#1b1b1d;--bcm-pagination-item-border-color:#3e3e42} ^.bcm_fp_features{--bcm-filter-results-features-item-color:#f5f5f5;--bcm-filter-results-features-item-bg-color:#161618} ^.bcm_checkbox{--bcm-checkbox-mark-bg-color:#252527;--bcm-checkbox-mark-border-color:#4c4c52;--bcm-checkbox-mark-icon-fill:#bcbcbc;--bcm-checkbox-text-color:#bcbcbc;--bcm-checkbox-disabled-mark-bg-color:#1b1b1d;--bcm-checkbox-disabled-mark-border-color:#3c3c40;--bcm-checkbox-checked-text-color:#f5f5f5} ^.bcm_radio{--bcm-radio-icon-color:#e17085} ^.bcm_range{--bcm-range-values-color:#bcbcbc;--bcm-range-track-bg-color:#1a1a1c;--bcm-range-handle-bg-color:#121213;--bcm-range-handle-border-color:#45454a} ^.bcm_form_alert{--bcm-form-alert-error-color:#ff5252;--bcm-form-alert-error-bg-color:#19191b} ^.bcm_accordion{--bcm-accordion-title-color:#c1c1c1;--bcm-accordion-arrow-fill:#f5f5f5} ^.bc_nav_tabs_wrp{--navigationTabs-item-color:#d8d8d8;--navigationTabs-inner-bg:#18181a;--navigationTabs-item-borderColor:#38383c;--navigationTabs_tags-after-bg:linear-gradient(270deg, #18181a, #f2f2f200)} ^.bc_fade_scroll{--fadeScroll-fade-bg:linear-gradient(90deg, #18181a, rgba(24,24,26,0)} ^.bc_fade_scroll.__listing_tags{--fadeScroll-fade-bg:linear-gradient(90deg, #121213, rgba(18,18,19,0)} ^.smileysContainer{--mobile-smiles-panel-container-background-color:#121213;--mobile-smiles-panel-container-border-color:#4c4c52;--mobile-smiles-panel-close-wrapper-color:#e36e84;--mobile-smiles-panel-title-wrapper-color:#e2e2e2;--mobile-smiles-panel-title-shortcut-color:#c0c0c0;--mobile-smiles-panel-tab-color:#d2d2d2;--mobile-smiles-panel-tab-background-color:#151516;--mobile-smiles-panel-tab-background-color-hover:#121213;--mobile-smiles-panel-tab-border-color:#353539;--mobile-smiles-panel-tab-active-background-color:#121213} ^.bcm_socials{--bcm-thm-socials-counter-color:#bcbcbc;--bcm-thm-socials-counter-bg:#121213;--bcm-thm-socials-counter-borderColor:#45454a} ^.bcm_switcher{--bcm-switcher-bg:#222225} ^.bc_notice{--bcm-notice-color:#e6e6e6;--bcm-notice-bg:linear-gradient(#121213, #121213);--bcm-notice-close-iconColor-hover:#dedede;--bcm-notice_secondary-paragraph-color:#d8d8d8;--bcm-notice_membershipError-close-iconColor-hover:#a960f0;--bcm-notice_sexToy-avatar-boxShadowColor:rgba(0,0,0,0);--bc-thm-notice_attention-bg:#181819;--bc-thm-notice_attention-color:#a866eb;--bc-thm-notice_topic-bg:#161618;--bc-thm-notice_topic-color:#e17085} ^.bc_picture_stub{--bcm-thm-pictureStub-iconColor:linear-gradient(180deg, #e07184 0%, #e36d84 100%)} ^.bc_carousel_bar{--carouselBar-item_active-inner-bg:#121213} ^.mbc_header{--mob-header-menuBtn-shadow-color:rgba(0,0,0,0);--mob-header-menuBtn-bg:#121213} ^.mbc_header_menu{--mob-header-menu-border-color:#4c4c52;--mob-header-menu-item-bg:#141416;--mob-header-menu-action-color:#f5f5f5} ^.mbc_header_menu.__sorting{--mob-header-menu-item-border-color:#49494e} ^.bcm_footer{--bcm-thm-footer-bg:#18181a;--bcm-thm-footer-borderColor:#2b2b2e;--bcm-thm-footer-details-color:#d8d8d8;--bcm-thm-footer-details-nav-link-color:#d8d8d8;--bcm-thm-footer-details-nav-link-bg-active:#1d1d1f;--bcm-thm-copyright-color:#d8d8d8;--bcm-thm-copyright-link-color:#df7187} ^.mls_item{--bcm-thm-listingThumb-stub-bg:#161617;--bcm-thm-listingThumb-stub-img-bg:#1d1d1f} ^.mls_tag_badge{--bcm-listing-tagBadge-bg:#18181a;--bcm-listing-tagBadge-borderColor:#3e3e42;--bcm-listing-tagBadge-color:#f3f3f3} ^.lscl_item{--listing-category-item-color:#f3f3f3;--listing-category-item-bg-color:#1c1c1e;--listing-category-item-firstRow-color:#ececec;--listing-category-item-firstRow-bg:#18181a;--listing-category-item-firstRow-bg-active:#212124} ^.listing_age_verification_banner{--ageVerificationBanner-icon-color:linear-gradient(#e26e84, #e17084)} ^.mls_hash_tag{--mlsHashTag-tag-color:#ececec;--mlsHashTag-tag-bg:#18181a;--mlsHashTag-tag-bg-active:#212124;--mlsHashTag-tag_record-icon-color:#bdbdbd;--mlsHashTag-removeFilterLink-close-iconColor:#d4d4d4} ^.instant_buy{--bcm-instant-buy-bar-border-color:#4c4c52;--bcm-instant-buy-success-wrp-circle-border-color:#4c4c52;--bcm-instant-buy-success-wrp-block-bg:#121213;--bcm-instant-buy-menu-bg:#161617;--bcm-instant-buy-menu-block-bg-active:#1e1e20;--bcm-instant-buy-menu-item-color:#eeeeee;--bcm-instant-buy-menu-item-desc-color:#c5c5c5} ^.bc_chat_topic{--bcm-chatTopic-bg:#121213;--bcm-chatTopic-inner-bg:rgba(22,22,24,0.9);--bcm-chatTopic-content-color:#e6e6e6;--bcm-chatTopic-title-color:#e17085} ^.chat_notice{--bcm-chatNotice-color:#e6e6e6;--bcm-chatNotice-inner-bg:#121213;--bcm-chatNotice-link-color:#52a1ff;--bcm-chatNotice-author-name-color:#e6e6e6} ^.chat-panel .history .service_list{--sysMsg-serviceList-bg:#121213;--sysMsg-serviceList-authorName-color:#e6e6e6;--sysMsg-serviceList-title-color:#f5f5f5;--sysMsg-serviceList-services-item-bg-hover:#181819;--sysMsg-serviceList-item-title-color:#e6e6e6;--sysMsg-serviceList-showBtn-color:#d8d8d8;--sysMsg-serviceList-showBtn-bg:#121213;--sysMsg-serviceList-showBtn-borderColor:#38383c;--sysMsg-serviceList-showBtn-color-active:#f5f5f5;--sysMsg-serviceList-showBtn-borderColor-active:#49494e;--sysMsg-serviceList-showBtn-bg-active:#121213} ^.bcm_chat_footer{--bcm-chat-footer-item-border-color:#323235;--bcm-chat-footer-item_tipMenu_disabled-bg:#38383c} ^.bcm_history_scroll_up{--bcm-historyScrollUp-bg:#252527;--bcm-historyScrollUp-link-color:#e17085} ^.bcm_chat_auto_message{--bcm-chat-member-notification-borderColor:#45454a;--bcm-chat-member-notification-title-color:#e2e2e2} ^.video-panel-wrap{--videoPanelWrap-bg:#121213} ^.mbc_tabs{--bcm-tabs-item-color:#d8d8d8;--bcm-tabs-item-bg:#18181a;--bcm-tabs-item_selected-color:#d8d8d8;--bcm-tabs-item_selected-bg:#18181a} ^.bcm_nav_panel{--bcm-navPanel-bg:#18181a;--bcm-navPanel-item-color:#d8d8d8;--bcm-navPanel-item_active-color:#e36e84} ^.f_rptfmm .buy_tokens,^.f_rptsmm .buy_tokens{--bcm-base-buyTokens-color:#d8d8d8;--bcm-base-buyTokens-borderColor:#36363a;--bcm-base-buyTokens-error-color:#ff5272;--bcm-buyTokens-bg:#121213;--bcm-buyTokens-text_faded-color:#bcbcbc;--bcm-buyTokens-decline-text-bg:#161617;--bcm-buyTokens-paymentMethodCards-card-bankCounter-bg:#121213;--bcm-buyTokens-packages-checkbox_off-borderColor:#4a4a50;--bcm-buyTokens-packages-error-bg:#19191b;--bcm-buyTokens-confirmCode-title-accent-color:#e17085;--bcm-buyTokens-amountField-bg:#121213;--bcm-buyTokens-amountField_onlyLocalCurrenct-localCurrency-color:#d8d8d8;--bcm-buyTokens-choice-bg:#121213;--bcm-buyTokens-choice-iconRadioOff-bg:#313134;--bcm-buyTokens-select-iconWrp-bg:#131315;--bcm-buyTokens-stickyFooter-bg:#121213} ^.f_rptsmm .buy_tokens{--bcm-buyTokens-paymentMethodCards-moreBtn-iconWrp-bg:#161617;--bcm-buyTokens-morePaymentMethodsPopup-bg:#121213;--bcm-buyTokens-morePaymentMethodsPopup-listWrp-shadow-bg:linear-gradient(180deg, #161617 35%, rgba(22,22,23,0.8) 50%, #ebfff000 100%);--bcm-buyTokens-morePaymentMethodsPopup-item-bg:#161617;--bcm-buyTokens-morePaymentMethodsPopup-item_active-bg:#121213;--bcm-buyTokens-morePaymentMethodsPopup-item-bankCounter-bg:#121213} ^.bcm_popup{--bcm-popup-body-bg-color:#121213} ^.categories_popup{--bcm-popup-categories-main-bg-color:#121213;--bcm-popup-categories-list-wrp-border-color:#45454a;--bcm-popup-categories-item-icon-color:#e17085;--bcm-popup-categories-item-color:#d8d8d8;--bcm-popup-categories-item-bg-color-active:#1b1b1d;--bcm-popup-categories-item-counter-bg-active:#121213} ^.filter_popup{--bcm-popup-filter-main-bg:#121213;--bcm-popup-filter-features-title-color:#c1c1c1;--bcm-popup-filter-footer-borderColor:#45454a;--bcm-popup-filter-notice-color:#c1c1c1;--bcm-popup-filter-button-outline-bg:#2b2b2e;--bcm-popup-filter-button-outline-borderColor:#3f3f44} ^.search_popup{--bcm-popup-search-header-bg-color:#18181a} ^.mbc_search_block{--bcm-search-block-input-bg-color:#121213;--bcm-search-block-input-borderColor:#404044;--bcm-search-block-input-placeholder-color:#d8d8d8;--bcm-search-block-header-close-icon-color:#d8d8d8;--bcm-search-block-reset-button-bg-color:#1a1a1c;--bcm-search-block-reset-button-border-color:#45454a;--bcm-search-block-result-empty-color:#e17085;--bcm-search-block-result-recent-title-color:#e17085;--bcm-search-block-result-name-color:#f5f5f5;--bcm-search-block-result-border-color:#333337;--bcm-search-block-result-tag-color:#ececec;--bcm-search-block-result-tag-name-bg-color:#18181a;--bcm-search-block-result-tag-symbol-color:#e17085} ^#pop-sorting-popup{--bcm-thm-popSortingPopup-sortingItem-color:#f5f5f5} ^.categories{--bcm-categories-skeleton-bg:#151516} ^.contact_support_page{--bcm-contact-support-page-title-color:#e17085;--bcm-contact-support-page-text-color:#d4d4d4;--bcm-contact-support-page-form-label-color:#f5f5f5;--bcm-contact-support-page-item-border-color:#353539} ^.join{--bcm-join-base-color:#d8d8d8;--join-bg:#121213;--join_login-flashAlert-bg:#19191b;--join_login-redirect-bg:#212124;--join_login-redirect-borderColor:#45454a;--join_login-redirect-text-color:#d8d8d8;--join_login-redirect-text-shadowColor:rgba(0,0,0,0)} ^.join_form{--bcm-joinForm-color:#d8d8d8;--bcm-joinForm-termsOfUse-bg:#151516;--bcm-joinForm-termsOfUse-color:#cecece;--bcm-joinForm-termsOfUse-checkbox-borderColor:#45454a;--bcm-joinForm-termsOfUse-checkbox-bg:#121213;--bcm-joinForm-input-color:#f5f5f5;--bcm-joinForm-input-bg:#151516;--bcm-joinForm-input-borderColor:#4c4c52;--bcm-joinForm-formLink-color:#b6b6b6} ^.invisible_mode_overview{--bc-thm-invisibleModeOverview-content-color:#9099c1;--bc-thm-invisibleModeOverview-promoBlock-feature-bg:#151517;--bc-thm-invisibleModeOverview-switchBlock-box-bg:linear-gradient(180deg, #121213 0%, #141416 100%);--bc-thm-invisibleModeOverview-switchBlock-box-borderColor:#38383c;--bc-thm-invisibleModeOverview-switchBlock-title-color:#9099c1;--bc-thm-invisibleModeOverview-switcher-wrp-bg:linear-gradient(180deg, #b6bfd1 0%, #121213 100%)} ^.invisible_mode_overview_plank{--bc-thm-invisibleModeOverviewPlank-color:#cacaca;--bc-thm-invisibleModeOverviewPlank-bg:#151516} ^.im_conversations .imc_reply{--bcm-im-reply-bg:#121213;--bcm-im-reply-borderColor:#323235;--bcm-im-reply-quickSmiles-bg:#121213;--bcm-im-reply-quickSmiles-borderColor:#3d3d41} ^.im_conversations .imc_rf_textarea_wrapper{--bcm-im-replyForm-textareaWrapper-control-color:#c4c4c4;--bcm-im-replyForm-textareaWrapper-control-textShadowColor:rgba(0,0,0,0);--bcm-im-replyForm-textareaWrapper-placeholder-color:#c4c4c4} ^.im_conversations .imc_rf_textarea_error{--bcm-im-replyForm-textareaError-color:#ff5252} ^.im_conversations .imc_r_button{--bcm-im-replyButton_smilepicker-trigger-icon-stroke:#c4c4c4;--bcm-im-replyButton_smilepicker-trigger_active-icon-stroke:#f5f5f5} ^.im_conversations .imc_balance_restriction_alert{--im-balanceRestrictionAlert-color:#d4d4d4;--im-balanceRestrictionAlert-bg:#151516;--im-balanceRestrictionAlert-title-color:#e17085} ^.im_conversations .imc_kicked_alert{--im-kickedAlert-color:#d4d4d4;--im-kickedAlert-acent-color:#e17085;--im-kickedAlert-bg:#121213;--im-kickedAlert-borderColor:#45454a} ^.im_conversations .imc_notification{--im-notification-color:#e6e6e6;--im-notification_pink-accent-color:#ea67ce} ^.im_conversations .imch_contact_card{--im-contactCard-color:#d4d4d4;--im-contactCard-accent-color:#e17085;--im-contactCard-checkbox-color:#d8d8d8;--im-contactCard-bg-color:#121213;--im-contactCard-borderColor:#45454a} ^.imch_reactions{--im-reactions-item-bg:#121213;--im-reactions-item_own-bg:#161617;--im-reactions-item-focus-bg:#161617;--im-reactions-counter-color:#d2d2d2} ^.bc_switch_lang_popup{--bcm-thm-switchLangPopup-bg:#121213;--bcm-thm-switchLangPopup-close-color:#e17085;--bcm-thm-switchLangPopup-title-color:#e17085;--bcm-thm-switchLangPopup-noteText-color:#bdbdbd;--bcm-thm-switchLangPopup-note-bg:#141415;--bcm-thm-switchLangPopup-note-borderColor:#45454a;--bcm-thm-switchLangPopup-flagsGroup-itemCaption-color:#cecece} ^.mbc_alert{color:#d6d6d6;background:#121213} ^.mbc_alert.__info{color:#c9c9c9;background:#151516;border:1px solid #3f3f43} ^.mbc_alert.__warning{color:#ff5253;background:#171718} ^.bc_btn{color:#cecece;background:#252527} ^.bc_btn:active{background:#303033} ^.bc_btn.bc_btn_white{background:#121213;border:1px solid #3f3f44;color:#cecece !important} ^.bc_btn.bc_btn_white:active:not(.__disabled),^.bc_btn.bc_btn_white:hover:not(.__disabled){background:linear-gradient(#131315,#18181a)} ^.bc_btn.bc_btn_white.__disabled,^.bc_btn.bc_btn_blue.__disabled{background:#18181a;border-color:#36363a} ^.bc_btn.bc_btn_light{background:linear-gradient(#151516,#1a1a1c);border:1px solid #3f3f44;color:#cecece !important} ^.bc_btn.bc_btn_light:active:not(.__disabled),^.bc_btn.bc_btn_light:hover:not(.__disabled),^.bc_btn.bc_btn_light.__disabled{background:linear-gradient(#1a1a1c,#151516)} ^.bc_confirm_popup_wrp.__hint,^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog .dlg_subTitle,^.bc_confirm_popup_wrp .account_deleted_popup,^.bc_confirm_popup_wrp .questions_popup,^.bc_confirm_popup_wrp .bc_confirm_popup.__paid_chat_hint .bc_cp_body,^.bc_confirm_popup_wrp .cancel_account_confirm,^.bc_confirm_popup_wrp .bc_confirm_popup.__auto_message_chat_hint .bc_cp_body,^.bcm_radio.__background .bcm_r_input:checked~.bcm_r_text,^.bcm_input_row,^.forward_to_remote,^.im_conversations .pm_confirm_email .pmc_body,^.im_conversations .pm_confirm_email .pmc_body .pmc_item,^.im_conversations .pm_offering .pmob_item,^.im_conversations .pm_offering .pmol_item{color:#d4d4d4} ^.bc_confirm_popup_wrp.__ask_again .bcc_header .bcc_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__error .bcc_container_info,^.bc_confirm_popup_wrp .bcc_no_header_popup .nh_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog.__d_alert .dlg_title,^.bc_confirm_popup_wrp .account_deleted_popup .adp_title,^.bc_confirm_popup_wrp .questions_popup .qp_title,^.bc_confirm_popup_wrp .questions_popup .qp_accent,^.bc_confirm_popup_wrp .delete_msg .dm_accent,^.bc_confirm_popup_wrp .cancel_account_confirm .cac_title,^.bcm_input_row .bcm_i_label_required,^.mls_title .mlst_icon.__category,^.vip-payment .inner-vip .vip-blocks,^.suspect_login_page .slp_desc,^.bc_cmn_confirm_wrp.__danger .bc_confirm_popup .bcc_title,^.head_confirm_box .hcb_close_wrp .hcb_close,^.aChat .message.admin strong,^#admin_chat_popup .admin_info .details .label,^.buy_tokens h4 strong,^.verify_info_box p b,^.confirm_box .line b,^.confirm_box .note span,^.bt_first_step span,^.buy_tokens .discount_gift_card .new_price,^.loyalty_box.leaving .lb_blue,^.account_preview .acp_alert,^.forward_to_remote .ftr_highlight,^.imch_reply .imch_r_author,^.imc_footer_reply .imc_fr_username,^.imc_footer_reply .imc_fr_close_icon,^.im_conversations .pm_valentine_day_themed_gift .pmvdtgd_accent,^.cg_vibe_with_me .vwm_s_danger{color:#e17085} ^.bc_confirm_popup_wrp.__ask_again .cgr_label,^.bc_confirm_popup_wrp.__ask_again .cg_label,^.bc_confirm_popup_wrp.__ask_again .tknt_item,^.bc_confirmer .bcc_alert .ca_text,^.bcm_filter_result .fr_title,^.chat-panel .msg_chat_game .cg_msg_s_time,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_title,^.confirm_box .example_box,^.cg_overlay .cg_options_list .ol_item .cgo_label,^.cg_overlay .cg_accordion .cga_item .ai_title,^.cg_overlay .cg_adj_empty_playlist .ep_message,^.cg_overlay .cg_adj_list .it_title,^.cg_overlay .cg_adj_notice,^.cg_overlay .cg_adj_title,^.cg_overlay .cg_select_subtitle .ss_chosen{color:#c1c1c1} ^.bc_confirm_popup_wrp.__ask_again .cgr_label .bc_capitalize,^.bc_confirm_popup_wrp.__ask_again .cg_label .bc_capitalize,^.bc_confirm_popup_wrp.__ask_again .tknt_item .bc_capitalize,^.bc_confirm_popup_wrp .bc_confirm_popup.__display_name,^.bc_confirm_popup_wrp .bcc_header .bcc_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__change_displayname .bcm_input_row .bcm_form_input,^.mbc_menu .mbcm_item.__default,^.translator_settings .bcf_check,^#mainbar .mm_icon,^#mainbar.__lang_list_opened .change-lang>.change-lang-btn,^.bcm_radio .bcm_r_input:checked~.bcm_r_text,^.mls_container.is_history .mlshiw_icon,^.vip-payment .head-vip i,^.vip-payment .head-vip,^.buy-descr,^#bChat .member,^.history p.model,^.history dfn:after,^.bcm_chat_selection .bcm_cs_title,^.bcm_chat_selection .bcm_cs_arrow,^.chat-panel .history .system_msg.msg_activity .mat_accent.__member,^.chat-panel .msg_chat_game.__vibe_with_me .cg_vwm_text,^.chat-panel .msg_chat_game .gc_msg_label,^.ic_menu_wrp .iw_translator,^.ic_menu_wrp .iw_font_picker,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_title .bc_capitalize,^.head_confirm_box .hcb_text,^.buy_tokens .buy_tokens_form p.form_notice,^.buy_tokens h4,^.processors .ui-btn-inner,^.buy_tokens .choice.discount_box .discount_sum,^.buy_tokens label.choice.discount_box .discount_sum,^.buy_tokens .bt_payment_title,^.buy_tokens .sp_current_package .spcp_local_currency,^.buy_invisible_package .choice_title,^.buy_invisible_package .choice_footer,^.bcm_popup .bcm_p_content,^.mbc_search_block .sb_input,^.mbc_search_block .sb_result .sbr_title,^.account_preview .acp_name,^.im_conversations .pm_confirm_email,^.smilePanelTabs .sm_t_label,^.cg_overlay .cg_options_list .ol_item .cgo_label .ol_accent,^.cg_overlay .cg_result .cgr_preview,^.cg_overlay .cg_accordion .cga_item .ai_arrow,^.cg_overlay .cg_adj_form .djf_title,^.cg_overlay .cg_adj_list .pll_item.__playing .it_title,^.cg_overlay .cg_select_subtitle,^.cg_overlay .cg_select_subtitle .ss_chosen .ssc_amount,^.cg_vibe_with_me.__vwm_success,^.cg_vibe_with_me.__vwm_success .vwm_s_desc,^.cg_vibe_with_me .vwm_text{color:#f5f5f5} ^.bc_confirm_popup_wrp .bc_confirm_popup,^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog .bc_cp_body,^.mbc_fullscreen_popup,^.what_view .what_view_close:before,^.bongacams_popup_v2 .popup_close:before,^.mls .mls_pager_header .mls_header_content,^.footer .support_group li.ui-btn-up-c,^.padded.white,^.padded.stripe:nth-child(2n),^.vip-payment .inner-vip,^#conversations .notfound,^#bChatFullScreenHistory p,^#chat_bar_v2_container,^.chat_prompt_wrp .cp_msg .cp_msg_decline_close:after,^.chat_prompt_wrp .cp_msg .cp_msg_decline_close:before,^.tap_model_popup_container .tap_container_inner,^.fancybox-tip-popup-wrap .fancybox-inner,^.bc_cmn_confirm_wrp .bc_confirm_popup,^.promo_in_banner_popup .pibp_body,^#admin_chat_popup .admin_chat_popup_content,^.buy_tokens .section .box,^.buy_tokens .bt_anim_icon_success:after,^.profile_avatar_popup,^.ac_change_photo .ac_cp_inner,^.imc_footer,^.imc_footer_alerts,^.im_conversations .imc_avatar .imc_avatar_img_wrap.__moderator,^.cg_overlay .cg_body,^.cg_keno_cube.__selected,^.bc_token_keno_field.__win .tkf_cube:hover .fc_body,^.bc_token_keno_field .tkf_cube.__checked:hover .fc_body,^.bc_token_keno_field .tkf_cube.__checked .fc_body,^.cg_overlay .cg_prize_list.__keno .pl_item.__open{background-color:#121213} ^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_check,^.bcm_radio.__background,^.bcm_checkbox_row .cr_label,^.cg_overlay .cg_input_wrp .cgi_label{background:#1c1c1e} ^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_text,^.cg_overlay .cg_input_wrp .cgi_text{color:#cecece;text-shadow:0 1px transparent} ^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_mark,^.cg_overlay .cg_input_wrp .cgi_mark{background:#2a2a2d} ^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_mark:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__buy_fb .bc_cp_close:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__buy_fb .bc_cp_close:after,^.bc_confirm_popup_wrp .bc_confirm_popup.__thank .bc_cp_close:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__thank .bc_cp_close:after,^.bc_confirm_popup_wrp .bc_confirm_popup.__feedback .bc_cp_close:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__feedback .bc_cp_close:after,^.bc_confirm_popup_wrp .account_deleted_popup .bcc_content,^#mainbar .mm_icon.__free,^.bc_confirmer .bcc_alert,^.holiday_discount_alert .hda_perc,^.bcm_skeleton:before,^.bcm_toggle_switcher_holder .ts_switcher:before,^.bcm_switcher.__on .bcm_s_slider:before,^.bcm_switcher .bcm_s_slider:before,^.info_part .icon_membership.icon_free,^.close_icon_wr span:before,^.close_icon_wr span:after,^.chat_prompt_wrp .cp_msg,^.chat_tooltip .close-btn:before,^.chat_tooltip .close-btn:after,^.bcm_oops_popup_layover .opl_body,^.popup_c2c_no_access .c2c_na_content,^.popup_c2c_no_access .c2c_na_close:before,^.popup_c2c_no_access .c2c_na_close:after,^.head_confirm_box .hcb_links .input_wrapper .ui-input-text,^.head_confirm_box .hcb_links .input_wrapper .hcb_input.ui-input-text,^.phone_confirm_box,^.sp_dropdown,html.bc-dark-m,^.buy_tokens .packages_badge .pb_icon,^#page_loader_overlay,^.loyalty_box.__personal .lb_holder,^.buy_tokens_header_board,^.bt_select_modal .slm_item,^.fancybox-wrap.__shemale_gold_slider_wrap .fancybox-close:before,^.fancybox-wrap.__shemale_gold_slider_wrap .fancybox-close:after,^.mbc_search_block .sb_result,^.popHint,^.vip_overview_stock_page .spp_list .pl_item,^.invisible_mode_overview .imsw_switcher:before,^.page_im.__settings .im_conversations,^.im_conversations .imc_actions .mbc_btn,^.im_conversations .imc_empty .imce_icon_wrp,^.im_conversations .imci_wrap,^.im_conversations .imc_followers .imcf_item,^.im_conversations .imc_search.__active .search_input,^.im_conversations .imc_search .search_input:focus,^.imch_reply,^.pm_activation_2fa .pmaf_alert,^.im_conversations .pm_upgraded_membership.__diamond .pmuh_logo_plank,^.im_conversations .pm_downgrade_membership .dmh_logo_plank,^.notice_need_tokens.__alert,^.smilePanel.smilePanelMobile .smilePanelContent,^.cg_overlay .gc_btn.__white,^.cg_overlay .cg_input_wrp .cgi_mark:before,^.cg_overlay .cg_game_field.__dice:before,^.bc_token_keno_field.__win .tkf_cube .fc_body,^.cg_vibe_with_me.__vwm_app .vwm_ai_title:after{background:#121213} ^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_control{color:#d8d8d8;border:1px solid #4c4c52} ^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .form_error,^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy .bc_cp_body .form_error,^.form .form_errors_row .form_error,^#bChat .msg_wrp .ch_m_tr_error,^.confirm_code_form strong.hl,^.im_conversations .imc_history_box .imch_item.__translate.__error .imch_msg_translate,^.cg_overlay .cg_result .cgr_prize .rp_text{color:#ff5252} ^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy.__profile_lg .bc_cp_body .bcf_input_block{background-color:#151516;border-color:#4c4c52} ^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy .bc_cp_body .bcf_label_wrp,^.ui-btn-hover-c:visited,^.ui-btn-hover-c:hover,^.ui-btn-hover-c a.ui-link-inherit,^.ui-btn-down-c:visited,^.ui-btn-down-c:hover,^.ui-btn-down-c a.ui-link-inherit,^.ui-btn-active:visited,^.ui-btn-active:hover,^.ui-btn-active a.ui-link-inherit,^.what_view.__pin .what_view_inner,^.bcm_checkbox_row .cr_text,^.phone_confirm_box .hcb_text,^.password_changed_box,^.loyalty_box.speaker .lb_first,^.loyalty_box.leaving,^.mbc_search_block .sb_result .sbr_delete_icon,^.profile_avatar_popup .account_actions .mbc_btn.ac_a_btn_cancel,^.account_actions .mbc_btn.ac_a_btn_cancel{color:#cecece} ^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy .bc_cp_body .bcf_input_block,^.live_tabs .tabs_list .tabsl_item.ui-btn-active{background-color:#1c1c1e} ^.bc_confirm_popup_wrp .bc_confirm_popup.__error .bcc_button{color:#cecece;background-color:#1a1a1c} ^.bc_confirm_popup_wrp .bcc_actions{border-top:1px solid #49494e} ^.bc_confirm_popup_wrp .bcc_no_header_popup{color:#d4d4d4;background:#151516} ^.bc_confirm_popup .cp_signup_wrap{color:#d8d8d8;background-color:#212124;border-top:1px solid #45454a} ^.bc_confirm_popup .cp_signup_title,^.ui-bar-e,^.ui-body-e,^.ui-overlay-e,^.video-item .date,^.model_status .author,^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper .gift_card_promo_code_valid,^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_bonuses .gift_card_promo_bonus{text-shadow:0 1px transparent} ^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog .dlg_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__change_displayname .bcm_i_label_txt,^.ui-body-b .ui-link-inherit,^.ui-body-c .ui-link-inherit,^.ui-bar-e .ui-link-inherit,^#mainbar .menu_item .mi_list .ui-btn,^#mainbar .menu_item .mi_list .mil_item.__collapsed .mil_sub_item,^#mainbar .combobox-lang,^#mainbar .combobox-lang .mm_arrow,^#mainbar .home .mm_live,^.auto_purchase_form,^.input_wrapper.__field_password .fp_icon_wrp,^.mls_title,^.notfound p,^.chat-panel .history .system_msg.msg_activity,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_content,^.bt_first_step,^.buy_tokens .buy_tokens_auto_purchase,^.loyalty_box.leaving .lb_second,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .action_block,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .action_block,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_title,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_title,^.buy_tokens .bt_decline .btd_desc,^.ac_change_photo,^.ac_change_photo .ac_cp_sub_title,^.im_conversations .imc_history_box .imch_item.__translate.__error,^.im_conversations .imc_history_box .imch_item.__tip .imch_msg_text,^.im_conversations .imc_history_box .imch_msg_translate,^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_name,^.im_conversations .pm_valentine_day_themed_gift .pmvdtgd_text,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_icon,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_icon_wrp{color:#d8d8d8} ^.bc_confirm_popup_wrp .questions_popup .qp_title.__warn{color:#f06171} ^.bc_confirm_popup_wrp .questions_popup .qp_accent_info,^.mls_container.is_history .mls_history_icon_wrp,^.chat_user_menu .cum_content,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description .description_block,^.buy_tokens .bt_extened_description_block,^.im_conversations .imc_history_box .imch_badge_unread,^.notice_need_tokens .nnt_icon_wrp{background:#161617} ^.bc_confirm_popup_wrp .questions_popup .qp_accent_info .ai_title{color:#5ff281} ^.bc_confirm_popup_wrp .questions_popup.__support .qp_title,^.ui-body-e .ui-link-inherit,^.what_view .what_view_inner .title,^.msg_style_panel .msp_subtitle,^.bcm_chat_invite.__decline .bcm_ci_badge_icon{color:#e2e2e2} ^.bc_confirm_popup_wrp .delete_msg .dm_text,^.seo_box h2,^.seo_box h3,^.mls_title .mlst_icon.__recordable,^.tap_model_popup_container .tip_after_private .tap_title_block{color:#bdbdbd} ^.bc_confirm_popup_wrp .bc_confirm_popup.__up_status .bc_cp_body,^.popup_c2c_no_access .c2c_na_text,^.buy_tokens_redirect .btr_text.__action,^.im_conversations .imc_actions,^.im_conversations .imc_actions .mbc_btn.__btn_msg.__active .imca_text.__new_msg,^.im_conversations .imc_actions .mbc_btn.__btn_msg:active .imca_text.__new_msg,^.im_conversations .imc_actions .imca_text.__online,^.empty_conversation .ec_inner,^.im_conversations .imc_search.__active .imcs_icon,^.im_conversations .imc_search .search_input:focus+.imcs_icon,^.notice_need_tokens .nnt_text{color:#d6d6d6} ^.bc_confirm_popup_wrp .bc_confirm_popup.__up_status .ups_desc{background-color:#141416} ^.bc_confirm_popup_wrp .bc_confirm_popup.__help .bcc_button{color:#cecece;background:linear-gradient(#151516,#1a1a1c);border:1px solid #3f3f44} ^.bc_confirm_popup_wrp .bc_confirm_popup.__pin_list .bc_cp_body,^.bc_online_notification .ont_avatar_wrp,^.bc_online_notification .ont_info_wrp,^.bc_context_menu_list .cml_item:active,^.bcm_skeleton .bcms_inner,^.bc_body.tablet.android .chat_panel_area:after,^.bcm_chat_selection .bcm_cs_link:hover,^.bcm_chat_selection .bcm_cs_link:active,^.cg_overlay .cg_footer{background:#18181a} ^.ui_floating_tooltip .ft_arrow{fill:rgba(245,245,245,0.8)} ^.mbc_menu{background-color:#121213;border:1px solid #38383c} ^.mbc_menu .mbcm_item{border-bottom:1px solid #38383c} ^.mbc_menu .mbcm_item:active,^.im_conversations .imc_history_box .imch_item.__selected,^.im_conversations .pm_upgraded_membership.__diamond .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__diamond .pmu_footer{background:#1b1b1d} ^.ui-body-b,^.ui-overlay-b{color:#e2e2e2;text-shadow:0 1px transparent;background:#18181a;background-image:-moz-linear-gradient(#232325,#2b2b2e);background-image:-ms-linear-gradient(#232325, #2b2b2e);background-image:linear-gradient(#232325,#2b2b2e);border:1px solid #4c4c52} ^.ui-bar-c{color:#d2d2d2;text-shadow:0 1px transparent;background:#1a1a1c;background-image:-moz-linear-gradient(#19191b,#232325);background-image:-ms-linear-gradient(#19191b, #232325);background-image:linear-gradient(#19191b,#232325);border:1px solid #4c4c52} ^.ui-bar-c .ui-link-inherit{color:#d2d2d2} ^.ui-body-c,^.ui-overlay-c{color:#d8d8d8;text-shadow:0 1px transparent;background:#151516;background-image:-moz-linear-gradient(#151516,#1a1a1c);background-image:-ms-linear-gradient(#151516, #1a1a1c);background-image:linear-gradient(#151516,#1a1a1c);border:1px solid #4c4c52} ^.ui-btn-up-c{color:#cecece;background:#141416} ^.ui-btn-hover-c,^#mainbar .li_shadow.ui-btn-active,^.ui-btn-active,^#mainbar .ui-btn-up-c.ui-btn-active,^#mainbar .ui-btn-active.ui-btn-hover-c,^#mainbar .ui-btn-active:focus,^#mainbar .ui-btn-active:active,^#mainbar .ui-btn-active:hover,^#mainbar .ui-btn-active,^#mainbar a.join-btn.ui-btn-active,^#mainbar .ui-btn-up-c.ui-btn-active .btn_ico.ui-link-inherit,^.buy_tokens .processors_btn_disabled{color:#cecece;background:#1c1c1e} ^.ui-btn-down-c{text-shadow:0 1px transparent;color:#cecece;background:#303033} ^.ui-btn-up-c,^.ui-btn-hover-c,^.ui-btn-down-c{background-color:#1c1c1e;color:#cecece !important} ^.ui-btn-active.ui-btn-up-c span.ui-btn-up-c,^.ui-btn-active.ui-btn-up-c a.ui-link-inherit,^.ui-btn-active.ui-btn-up-c,^.ui-btn-active.ui-btn-up-c:visited,^.ui-btn-active.ui-btn-hover-c:hover,^.ui-btn-active.ui-btn-down-c a.ui-link-inherit{background:#1c1c1e;color:#cecece !important} ^.ui-checkbox .ui-btn-up-c,^.choice.ui-btn-up-c,^.ui-listview .ui-btn-up-c,^.ac_settings .ac_delete_link_wrp{color:#cecece;background:#141416;border:1px solid #45454a} ^.ui-collapsible-heading-toggle.ui-btn-up-c{color:#cecece;border:1px solid #45454a;background:#141416 !important} ^.ui-checkbox .ui-btn-hover-c,^.choice.ui-btn-hover-c,^.ui-listview .ui-btn-hover-c,^.ui-collapsible-heading-toggle.ui-btn-hover-c{color:#cecece;background-color:#1c1c1e;border:1px solid #45454a} ^.ui-body-d,^.ui-overlay-d,^#mainbar .bc_social_networks .sn_count,^.aChat,^#admin_chat_popup .admin_info .admin_avatar_wrp,^.loyalty_box,^.buy_tokens_sms_dimoco_package .input_phone,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_input,^.buy_tokens .sp_input,^.im_conversations .pm_confirm_email .pmc_form,^.cg_overlay .cg_adj_list .djpl_list{border:1px solid #45454a} ^.ui-btn-up-e:visited,^.ui-btn-up-e a.ui-link-inherit,^.ui-btn-up-e,^.ui-btn-hover-e,^.ui-btn-down-e,^.ui-btn-active.ui-btn-up-e:visited,^.ui-btn-active.ui-btn-hover-e:hover,^.ui-btn-active.ui-btn-down-e a.ui-link-inherit{color:#feb953} ^.ui-icon-alt .ui-icon,^.ui-icon-alt .ui-icon-searchfield:after{background-color:rgba(18,18,19,0.3)} ^.buy_tokens label.selected,^.live_tabs .ui-btn-active.ui-btn-up-c .ui-btn-up-c,^.categories .ui-btn-active.ui-btn-up-c .ui-btn-up-c,^.footer .support_group li.ui-btn-up-c.support_message,^.im_conversations .imc_actions .bcf_selectbox:active,^.im_conversations .imc_actions .mbc_btn.__active,^.im_conversations .imc_actions .mbc_btn:active{background:#141416} ^.form div.ui-input-text input.ui-input-text,^.input_wrapper.__field_password .ui-input-text,^.bcm_input_row .bcm_form_input,^.bcm_input_row .bcm_form_textarea,^.tap_model_popup_container .tip_after_private .radio_list li .custom_price input,^.head_confirm_box .hcb_links .input_wrapper{border:1px solid #4c4c52} ^.what_view{color:#d4d4d4;background-color:#121213;border:1px solid #45454a} ^.what_view .what_view_inner{background-color:#151516} ^.what_view .what_view_corner{background:#121213;border-color:#0000 #0000 #45454a #45454a} ^.translator_settings .mbc_select{border:1px solid #3c3c40} ^.msg_style_panel .preview_content,^.mbc_search_block.__filter .sb_input,^.mbc_search_block.__filter .sb_result{border:1px solid #3f3f43} ^.msg_style_panel .msp_p_item.__selected,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_input,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_input{border-color:#3d3d41} ^.bongacams_popup_v2{background-color:#121213;border:1px solid #4c4c52} ^#mainbar,^#mainbar .ui-panel-inner,^body,^.ui-page,^.ui-content,^.ui-panel-content-wrap{background:#121213 !important} ^#mainbar .ui-listview .mc_followers .btn_icon .mc_icon_follower{fill:#d8d8d8;stroke:#d8d8d8} ^#mainbar .ui-li-divider,^.ic_menu_wrp .icm_item,^.buy_tokens_sms_form h3,^.buy_tokens_sms_sale_form h3,^.buy_tokens_wire_form h3,^.cg_overlay .cg_accordion .cga_item,^.cg_overlay .cg_adj_list.__no_btn .pll_item:last-child{border-bottom:1px solid #45454a} ^#mainbar .ui-li{border-color:#3e3e43} ^#mainbar .ui-btn-active,^#mainbar .mainbar-control.__end_position,^#bChat .historyContainer,^.buy_tokens_wire_first h4{border-top:1px solid #45454a} ^#mainbar .ui-btn-active.ui-btn-up-c .ui-link-inherit .mm_icon.__free{border-color:#47474c} ^#mainbar .btn_icon{border-top:1px solid #3e3e43} ^#mainbar .menu_item,^.footer_content .copy a{color:#f5f5f5 !important} ^#mainbar .menu_item.__collapsed,^.bc_header_live_tabs .hlt_link:visited,^.bc_header_live_tabs .hlt_link,^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_text,^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_signature{color:#d8d8d8 !important} ^#mainbar .menu_item .mi_list .mil_sub_item .mm_arrow,^.head_confirm_box .hcb_close_wrp.__bubble .hcb_close_icon{fill:#e17085} ^#mainbar .language-dropdown{background:#121213;border-top:1px solid #45454a} ^#mainbar .language-dropdown .change-lang-btn.__selected,^#mainbar .language-dropdown .change-lang-btn:active,^.im_conversations .pm_upgraded_membership.__sapphire .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__sapphire .pmu_footer,^.im_conversations .pm_upgraded_membership.__titan .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__titan .pmu_footer,^.im_conversations .pm_upgraded_membership .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership .pmu_footer{background:#19191b} ^.bc_online_notification .ont_close_wrp .ont_close,^.bc_online_notification .ont_info .ont_i_text,^.chat-content .bcm_paid_show_countdown .bcm_psc_text,^.bcm_chat_footer .cf_item,^.bcm_chat_footer .cf_item:hover .cfi_inner,^.buy_tokens .bt_cashback_nft_wrp,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_title,^#buy_tokens_sms_confirm_code_form_tele2 .cct_form,^.buy_tokens .sp_label{color:#c4c4c4} ^.bc_confirmer .bcc_alert .caa_button,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_actions .bcc_button.__light{color:#cecece;background:#1a1a1c} ^.bc_header_live_tabs .hlt_inner,^.smileButton.ui-btn-hover-e,^.tap_model_popup_container .tip_after_private .form_actions,^.forward_to_remote .ftr_header{background-color:#18181a} ^.holiday_discount_alert.__world_sexual_health_day_2026 .hda_buy{color:#596ef8} ^.bc_ms_icon.__diamond{fill:#f65d5b} ^.bc_ms_icon.__exclusive{fill:#efefef} ^.bc_ms_icon.__sapphire{fill:#9452ff} ^.bc_ms_icon.__white_bg.__diamond{color:#f65d5b} ^.bc_ms_icon.__white_bg.__exclusive{color:#efefef} ^.bc_ms_icon.__white_bg.__sapphire{color:#9452ff} ^.bc_context_menu_list,^.im_conversations .imc_actions .bcf_selectbox .bcsb_dropdown,^.bcsb_dropdown.__in_portal,^.im_conversations .imc_history_box .imch_item.__action_btns .imch_msg_actions{background:#121213;border:1px solid #38383c} ^.bc_context_menu_list .cml_item,^.im_conversations .imc_actions .bcf_selectbox .bcsb_item,^.bcsb_dropdown.__in_portal .bcsb_item{color:#f5f5f5;border-bottom:1px solid #38383c} ^.auto_purchase_form .bcf_control{background:#151516;border:1px solid #4c4c52} ^.bc_dropdown.__reactions .mbcm_item+.mbcm_item{border-left:1px solid #38383c} ^.bcm_radio .bcm_r_input:disabled~.bcm_r_mark{background:#1b1b1d;border-color:#3c3c40} ^.bcm_radio .bcm_r_input:disabled~.bcm_r_mark:before,^.bcm_toggle_switcher_holder .ts_switcher{background:#222225} ^.bcm_radio .bcm_r_mark{background:#252527;border:1px solid #4c4c52} ^.bcm_radio .bcm_r_text,^.mls_item_online.actions a.chat,^.bcm_filter_result .fr_value,^.purchase_vip label.choice.discount .eur-vip s,^.history .service.__group .msgs_desc,^.chat_prompt_wrp .cp_msg.__group_join .cp_msg_s_text,^.chat_prompt_wrp .cp_msg.__group_join .cp_msg_arrow .cp_msg_arrow_icon,^.bcm_chat_selection .bcm_cs_desc,^.bcm_chat_selection .bcm_cs_price,^.buy_tokens_wire_form p,^.buy_tokens_sms_dimoco_form p,^.buy_tokens_digiseller_form p,^#buy_tokens_payment_type .ui-collapsible .package.__simple .brbw_input:disabled~.ui-btn .ui-btn-text,^#buy_tokens_payment_type .bt_radio_btn_wrp.__simple .brbw_input:disabled~.ui-btn .ui-btn-text,^.buy_tokens .choice .ui-btn-inner,^.buy_tokens .choice .foreign_currency,^.buy_tokens .bt_price_old,^.buy_tokens .pkg_cybersale .bt_strike_price,^.buy_tokens .bt_payment_description,^.buy_tokens_sms_dimoco_package .input_phone.__sending .status,^.buy_tokens_sms_dimoco_package .input_phone .sdp_phone_title,^.buy_tokens_sms_dimoco_package .form_note_btn,^.f_rptfmm .buy_tokens_sms_dimoco_package .package+p,^.f_rptsmm .buy_tokens_sms_dimoco_package .package+p,^.f_rptfmm .buy_tokens_sms_dimoco_package .package+p+p,^.f_rptsmm .buy_tokens_sms_dimoco_package .package+p+p,^.buy_tokens .sp_current_package .spcp_approx,^.buy_tokens .bt_custom_amount_field,^.buy_tokens .bt_custom_amount_field .caf_input_custom_field,^.buy_invisible_package .choice_description,^.im_conversations .imc_history_box .imch_item.__translate,^.im_conversations .imc_history_box .imch_item.__action_btns .imch_action_btn,^.cg_overlay .cg_options_list .ol_item.__checked .cgo_radio{color:#bcbcbc} ^.bcm_radio.__background .bcm_r_text{color:#d4d4d4;text-shadow:0 1px transparent} ^.bc_phone_select_box .bcsb_input_wrp{color:#c4c4c4;border:1px solid #444449;background:#161617} ^.bc_phone_select_box .bcsb_input{color:#c4c4c4;background:#121213;border:1px solid #444449} ^.bc_phone_select_box .bcsb_input::placeholder,^.bcm_input_row .bcm_form_input::placeholder,^.bcm_input_row .bcm_form_textarea::placeholder,^.purchase_vip label.choice.discount_gift_card .eur-vip s,^.purchase_vip label.choice.discount .eur-vip s,^.mbc_search_block .sb_input::placeholder,^.im_conversations .imc_search .imcs_icon,^.im_conversations .imc_search .search_input::placeholder,^.imch_reply .imch_r_msg,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_code{color:#b3b3b3} ^.bcm_checkbox_row .cr_mark{background:linear-gradient(#2f2f33,#252528)} ^.bc_social_networks .sn_count{color:#bcbcbc;background:#121213} ^.range_slider .rs_track,^.im_conversations .imc_history_box .imch_date_wrp:before,^.im_conversations .imc_history_box .imch_date_wrp:after,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_icon_wrp.__vwm_active{background:#252527} ^.range_slider .rs_progress.__zero:after{background:#121213;border:1px solid #404045} ^.mls .mls_pager_header{color:#bcbcbc;border-top:1px solid #45454a} ^.bcm_filter_btn_wrp .bcm_fbw_btn{color:#cecece;background:#121213;border:1px solid #3f3f44} ^.bcm_filter_btn_wrp .bcm_fbwb_icon{color:#dadada} ^.mls_tabs_container{background:linear-gradient(#121213,#1a1a1c);border-bottom:1px solid #38383c} ^ul.mls_tabs li{text-shadow:1px -1px transparent;background-color:#19191b;border:1px solid #45454a} ^ul.mls_tabs li:hover,^.phone_confirm_box .confirm_phone_form .bc_phone_select_box.__focused .bcsb_input,^.phone_confirm_box .confirm_phone_form .bc_phone_select_box.__focused .bcsb_input_wrp{border-color:#4c4c52} ^ul.mls_tabs li a,^.im_conversations .pm_offering p{color:#d4d4d4 !important} ^.pager>div{background-image:linear-gradient(#29292c,#222224)} ^.forget_info,^.history p.undefined,^.history p.undefined dfn,^.aChat .message .col1,^.buy_tokens .gray_desc,^.verification_failed p,^.credit_box_success .gray_desc{color:#b2b2b2} ^.live_tabs .tabs_list .tabsl_item.__new .tabsl_count:before{background:linear-gradient(#141416,#141416) padding-box padding-box,linear-gradient(270deg,#ff8201,#fca710) border-box} ^.form .form_errors_row{background-color:#19191b} ^.footer_content{background-color:#222225} ^.footer_content .separator{background-color:#343438;border-top:1px solid #333336} ^.center-wrapper .descr_block{border:1px dashed #4c4c52} ^.icon_membership.icon_free{background:#121213;border:2px solid #47474c} ^.mc_status_icon.__icon_free{background-color:#121213;border:2px solid #47474c} ^.padded.grey,^.padded.stripe,^.buy_tokens_wire_form .odd,^.page_im .im_conversations .imc_action_bar{background-color:#1a1a1c} ^.vip-payment .ui-btn-up-c{border:2px solid #45454a !important} ^.vip-payment .head-vip{background-color:#141415;border-bottom:1px solid #45454a} ^.vip-payment.pkg_cybersale .eur-vip,^.vip-payment.pkg_cybersale .inner-vip .vip-blocks,^.maroon_strong,^.buy_tokens .pkg_cybersale .bt_highlight,^.buy_tokens .pkg_cybersale .foreign_currency,^.buy_tokens .bt_custom_amount_alert .caa_cell.__accent,^.buy_tokens.__holiday .bt_highlight{color:#dd7488} ^.vip-payment .eur-vip{color:#e17085;border-left:1px solid #444449} ^.vip-payment .eur-vip b,^.fancybox-error,^.buy_tokens .mobile_pay_form .rich,^.loyalty_box.speaker,^.loyalty_box.speaker .lb_white,^a{color:#cfcfcf} ^.vip-payment .head-vip b,^.notfound .nt_des,^.notfound p.nf_bottom a,^.notfound p.nf_bottom{color:#b4b4b4} ^.vip-payment .foot-vip{color:#b4b4b4;background-color:#141415;border-top:1px solid #45454a} ^.vip-exist,^.buy_tokens_warning,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description{border:1px solid #36363a} ^.votes_percent_line,^#bChat .msg_wrp .ch_m_translator_link.__source,^#bChat .msg_wrp .ch_m_translator_link.__translate,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_code_wrp{background:#121213;border:1px solid #45454a} ^.bitcoin_paypal_popup .close_button{color:#f5f5f5;background:#121213} ^.suspect_login_page form,^.vip_overview_page .vo_badge,^.im_conversations .imc_item.__selected .imci_wrap{background:#151516} ^.suspect_login_page .form_error{color:#ff5252;background-color:#19191b} ^#bChat .history p.deleted .msg{color:#b6b6b6 !important} ^#bChatFullScreenHistory p.groupChatWelcome,^.history p.groupChatWelcome{color:#5252ff !important} ^.history .service.__group{background:#141415;border:1px solid #38383c} ^.history p.topicChange{color:#e17085 !important} ^.history p.model .icon_avatar,^.imc_footer_reply .imc_fr_inner{background-color:#161618} ^.smileButton{background-color:#18181a;border:1px solid #3d3d41} ^.smileButton .smile_btn_icon,^.chat-content #bChatInputContainer .smile_btn_icon{stroke:#c4c4c4} ^.smileButton.__active .smile_btn_icon{stroke:#f5f5f5} ^#scroll-chat-btn .inner:after,^.popHint.topLeft:after,^.popHint.topRight:after,^.popHint.topCenter:after{border-top-color:#2b2b2e} ^.chat_prompt_wrp .cp_msg_after{background:linear-gradient(#202023 0%,#fff0 25%)} ^.chat-content .chat_panel_wrap>.bcm_ps_countdown_container .bcm_paid_show_countdown{border-top:1px solid #36363a} ^.chat-content .bcm_paid_show_countdown{background:#18181a;border-bottom:1px solid #36363a} ^.bcm_chat_selection{background:#121213;border:1px solid #3d3d41} ^.bcm_chat_selection .bcm_cs_list_item{border-bottom:1px solid #3d3d41} ^.chat-panel .history .system_msg.tip_success.__tip_menu .msg_clear_btn{border:1px solid #2b2b2e} ^.chat-panel .history .system_msg.follow_plank .fp_container{color:#e6e6e6;background:rgba(27,27,29,0.7)} ^.chat-panel .history .system_msg.msg_activity .ma_avatar_wrp:before{border:1px solid rgba(43,43,46,0.8)} ^.chat-panel .msg_chat_game,^#buy_tokens_payment_type .ui-collapsible .package .ui-btn.selected .ui-btn-inner,^#buy_tokens_payment_type .bt_radio_btn_wrp .ui-btn.selected .ui-btn-inner,^.bt_select_modal .slm_i_input:checked~.slm_i_label{background:#141415} ^.chat-content .cp_navigation .cpnl_item,^.im_conversations .imc_history_box .imch_item_wrp.__highlight:before{background-color:#212124} ^.chat-content #bChatInputContainer{background-color:#121213;border-top:1px solid #323235;border-bottom:1px solid #323235} ^.chat-content #bChatInputContainer .sendInput{color:#c4c4c4;background-color:#18181a} ^.chat-content #bChatInputContainer .sendInput::placeholder,^.chat-content #bChatInputContainer .sendInput.__placeholder:before{color:#c4c4c4 !important} ^.ic_menu_wrp .ic_menu{background:#151516;border:1px solid #45454a} ^#bChat .msg_wrp.__tr_enabled.__tr_show_tooltip{background:#171718} ^#bChat .msg_wrp .ch_m_translator_link.__empty_translate{color:#cecece;background:#171718} ^.chat_user_menu .cum_header{color:#d8d8d8;border-bottom:1px solid #38383c} ^.chat_user_menu .cum_info .mi_content,^.chat_user_menu .cum_action .ma_text{color:#b5b5b5} ^.bc_new_message_separator:before,^.bc_new_message_separator:after{background-color:#262628} ^.bcm_chat_footer{background:#18181a;border-top:1px solid #323235;border-bottom:1px solid #36363a} ^.bcm_chat_footer .cf_balance{color:#c4c4c4;background:#18181a} ^.bcm_chat_invite .bcm_ci_badge_icon,^.bcm_chat_invite .bcm_ci_toggle_icon{fill:rgba(245,245,245,0.2)} ^.video-panel .layover .layoverText .layoverIns,^.ru_buy_tokens_mobile_pay p.provider_attention{color:#c5c5c5} ^.video-panel .layover .layoverLimitText{color:#b2b2b2 !important} ^.layover_before_private .lbp_content{background-color:rgba(18,18,19,0.4)} ^.layover_before_private .lbp_item,^.bc_confirm_popup.fan_boosted_confirm .bcc_container_info{color:#d5d5d5} ^.bcm_oops_popup_layover .opl_message{color:#d8d8d8;background:#161618} ^.quality_settings_block .qsb_item.__active{background-color:rgba(18,18,19,0.2)} ^.cam2cam_modal{border:2px solid #444449} ^.tap_model_popup_container .tip_after_private .tap_title .tap_model_name{color:#dc748a} ^.tap_model_popup_container .tip_after_private .radio_list li{background-color:#121213;border:1px solid #45454a} ^.tap_model_popup_container .tip_after_private .radio_list li.selected{background:#131315} ^.tap_model_popup_container .tip_after_private .radio_list li .custom_price input:disabled{background-color:#121213;border-color:#3c3c41} ^.tap_model_popup_container .tip_after_private .radio_list li .radio_help{color:#a1a3b0} ^.tap_model_popup_container .tip_after_private .form_actions .tap_note{color:#e3e3e3} ^.tap_model_popup_container .tip_after_private .form_actions .tap_note .tap_red{color:#dc758a} ^.tap_model_popup_container .tip_after_private .form_error_list{color:#dc758a;background-color:#18181a} ^.fan_boosted_animation.__stopping .fba_timer{stroke:#ff527b} ^.bc_confirm_popup.fan_boosted_confirm .bcc_header .bcc_title{color:#f4f4f4} ^.fancybox-skin{color:#cfcfcf;background:#151516} ^.head_confirm_box{background-color:#131315} ^.head_confirm_box .hcb_close_wrp:hover .hcb_close{color:#e46d89} ^.head_confirm_box .hcb_close_wrp.__bubble:hover .hcb_close_icon{fill:#e46d89} ^.phone_confirm_box .fcb_not_show .ns_mark{border:2px solid #4c4c52} ^.phone_confirm_box .confirm_phone_form{color:#c9c9c9} ^.phone_confirm_box .confirm_phone_form .cpf_code .bcf_control{color:#cfcfcf;background:#121213;border:1px solid #4c4c52} ^.promo_in_banner_popup .pibp_name{color:#e17084 !important} ^#admin_chat_popup .admin_info .details h4{border-bottom:1px solid #4c4c52} ^.buy_tokens .bt_email_wrp,^.buy_tokens .sp_form{color:#c4c4c4;border:1px solid #414146} ^.buy_tokens .btc_input_warning{color:#f65a61} ^.buy_tokens .limit_alert,^.buy_tokens .buy_tokens_mbway_form .bt_input_wrp .err,^.buy_tokens .buy_tokens_wasabigate_form .bt_input_wrp .err,^.buy_tokens .buy_tokens_hkpay_form .bt_input_wrp .err,^.buy_tokens .coinpayments_form .bt_input_wrp .err.err_show,^.buy_tokens .mobile_pay_form .bt_input_wrp .err.err_show,^.buy_tokens .bt_custom_amount_field .caf_input_warning{color:#ff5272} ^.buy_vip_sms_form .price span,^.buy_vip_sms_form .price em,^.ru_buy_tokens_sms_pay .heading h4,^.ru_buy_tokens_mobile_pay .heading h4,^.ru_buy_tokens_sms_pay ul a{color:#db7688} ^.buy_tokens .mobile_pay_form input,^.confirm_code_form input{border:1px solid #45454a;border-color:#4c4c52 #45454a #45454a #4c4c52} ^.buy_tokens .mobile_pay_form input.disabled{color:#bcbcbc;background-color:#1a1a1c} ^.confirm_code_form{color:#bcbcbc;border:1px solid #45454a} ^table.wire-tab td{color:#f5f5f5;border:1px solid #45454a} ^.buy_tokens .foreign_currency span{color:#5297ff} ^.verify_info_box p,^.confirm_box{color:#d9d9d9} ^.confirm_box .example_box .ex{background-color:#121213;border:1px solid #3e3e43} ^.confirm_box .example_box .ex .amount span,^.confirm_box .example_box .info{color:#e86969} ^#buy_tokens_payment_type .ui-collapsible .package .ui-btn:after,^#buy_tokens_payment_type .bt_radio_btn_wrp .ui-btn:after,^.buy_tokens .bt_select_change:after{border:1px solid #414146} ^#buy_tokens_payment_type .payment_icon,^.bt_select_modal .payment_icon{background:#121213;border:1px solid #3e3e42} ^.buy_tokens .choice.discount_box,^.buy_tokens label.choice.discount_box,^.buy_tokens .sp_current_package{color:#bcbcbc;background:#131315;border:1px solid #45454a} ^.buy_tokens .sms_amount_error,^.buy_tokens .bitcoin_form .bitcoin_error,^.buy_tokens .bitcoin_form .info_text{color:#f25e5e} ^.buy_tokens .discount_box .original_sum,^.buy_tokens label.discount_box .original_sum,^.show_hide_packages{color:#b9b9b9} ^.gift_card_section .gift_card_box{background:#141416;border:1px solid #45454a} ^.buy_tokens .ui-radio label{background-color:#141416 !important} ^.buy_tokens .pkg_cybersale .ui-radio label,^.buy_tokens .bt_select_change{color:#bcbcbc !important} ^.buy_tokens_redirect .btr_text.__wait,^.buy_tokens_header_board .hbt_paragraph,^.im_conversations .imc_actions .imca_text.__new_msg,^.im_conversations .imc_empty .imce_text,^.im_conversations .imci_message,^.im_typing{color:#bababa} ^.loyalty_box.speaker:before{border-top:5px solid #2b2b2e} ^.loyalty_box.speaker:after{border-top:6px solid #45454a} ^.loyalty_box.bit{color:#cbcbcb;background:#141416} ^.loyalty_box.bit .lb_title{color:#eaeaea} ^.loyalty_box.bit .lb_green{color:#92ff52} ^.loyalty_box.bit .lb_warn:before{border-top:1px solid #3a3a3e} ^.loyalty_box.leaving .lb_warn:before,^.loyalty_box.leaving .lb_warn:after{background:#2b2b2e} ^.buy_tokens .confirm_code_form_resend{background:#131315;border:1px solid #45454a} ^.buy_tokens_header_board .hbt_headline_block,^.cg_service_list .cg_sub_header{border-bottom:1px solid #333336} ^.buy_tokens_header_board .hbt_headline{color:#e8e8e8} ^.buy_tokens .bt_custom_amount_alert{color:#bcbcbc;background:#131315;border:1px solid #414146} ^.buy_tokens .bt_custom_amount_field .caf_input_currency{color:#bcbcbc;background:#141415} ^.show_hide_packages .sh_label .sh_icon{color:#c5c5c5;background:#141416;border:1px solid #45454a} ^.buy_tokens .bt_anim_icon_success .ais_item:after{border:6px solid #2b2b2e} ^.bt_select_modal{background:#121213;border:1px solid #404044} ^.bt_select_modal .slm_i_label{background:#121213;color:#bcbcbc !important} ^#discount_green_purple .dgp_clock_hand{color:#c58bb5} ^.gc_popup{color:#9d78d8;background:#121213} ^.gc_popup .gc_title .gc_red{color:#e07185} ^#gc_popup_vip .gc_title .gc_red,^.ac_change_photo .ac_cp_desc.__error{color:#e36e84} ^.gc_popup.gc_popup_hor .gc_title{color:#9d78d8} ^.fancybox-item.fancybox-close.pl_un_default_close,^.fancybox-item.fancybox-close.di_ex_close{color:#f5f5f5;background:#121213 !important} ^.mbc_search_block.__filter .sb_result .sbr_head.__recent{border-bottom:1px solid #3f3f43} ^.mbc_search_block .sb_result .sbr_count{color:#c2c2c2} ^body{color:#cfcfcf;background:#242426} ^.popHint .popHintFooter{background:#141416;border-top:1px solid #3c3c41} ^.popHint.bottomLeft:after,^.popHint.bottomRight:after,^.popHint.bottomCenter:after{border-bottom-color:#2b2b2e} ^.popHint.leftTop:after,^.popHint.leftBottom:after,^.popHint.leftCenter:after{border-left-color:#2b2b2e} ^.popHint.rightTop:after,^.popHint.rightBottom:after,^.popHint.rightCenter:after{border-right-color:#2b2b2e} ^#bChat.bChatModel .popHint:after{border-top-color:#2f2f33} ^.profile_avatar_popup .account_actions,^.account_actions{background-color:#141415;border-top:1px solid #49494e} ^.profile_avatar_popup .account_actions .mbc_btn.ac_a_btn_cancel:active,^.account_actions .mbc_btn.ac_a_btn_cancel:active{background-color:#323235} ^.account_preview .acp_img_wrp{border:2px solid #2b2b2e} ^.account_preview .acp_btn{color:#cecece;text-shadow:0 1px transparent;background:#252527} ^.ac_change_photo .ac_cp_line{background-color:#2b2b2e} ^.vip_overview_page,^.vip_overview_page .vo_ttl{color:#c3c3c3} ^.page_content .cancel_subscription.__dashed{border:1px dashed #45454a} ^.vip_overview_page_badge{color:#c3c3c3;background:#151516} ^.vip_overview_stock_page{color:#e0e0e0;border:1px solid #313134} ^.vip_overview_stock_page .sp_privilege{border-top:1px solid #37373a} ^.invisible_mode_overview_plank{color:#9099c1;background:#151516} ^.page_im .im_conversations,^.smilePanelTabs{background:#1a1a1c} ^.im_conversations .imc_actions .bcf_selectbox,^.im_conversations .gift_card_promo_wrapper{background:#121213;border:1px solid #3e3e43} ^.im_conversations .imc_actions .mbc_btn.__btn_msg.__disabled{background:#2e2e31} ^.im_conversations .imc_actions .imca_icon{color:#d6d6d6;fill:#d6d6d6} ^.im_conversations .imc_item.__new .imci_wrap .imci_message,^.im_conversations .imci_user_name,^.im_conversations .imc_followers .imcf_name{color:#d7d7d7} ^.im_conversations .mbc_alert.__tip,^.im_conversations .imc_history_box .imch_group.__own .imch_item.__tip,^.im_conversations .pm_upgraded_membership.__unlimited .pmu_footer{background:#171719} ^.im_conversations .imc_history_box .imch_msg_check.__unsent .imch_mc_icon,^.im_conversations .imc_search .imcs_reset .imcs_r_icon{fill:#e36d84} ^.im_conversations .imc_history_box .imch_group.__own .imch_item,^.pm_activation_2fa .pmaf_body,^.im_conversations .pm_upgraded_membership.__exclusive .pmu_holder.__filled{background:#161618} ^.im_conversations .imc_history_box .imch_group.__own .imch_item:first-of-type:after{border-left:6px solid #303033} ^.im_conversations .imc_history_box .imch_group.__own .imch_item.__tip:first-of-type:after{border-left-color:#313134} ^.im_conversations .imc_history_box .imch_item{background:#151517} ^.im_conversations .imc_history_box .imch_item:first-of-type:after{border-right:6px solid #2f2f32} ^.im_conversations .imc_history_box .imch_item.__selected:first-of-type:after{border-right-color:#353538} ^.im_conversations .imc_history_box .imch_item.__sq .imc_question_link,^.im_conversations .imch_item.__default a{color:#52b0ff} ^.im_conversations .imc_history_box .imch_item.__sq_error{color:#e17085;background:#171719} ^.im_conversations .imc_history_box .imch_item.__sq_error:first-of-type:after{border-right-color:#313134} ^.im_conversations .imc_history_box .imch_item.__action_btns .imch_action_btn.__active{background:#202022} ^.im_conversations .imc_history_box .imch_context_menu,^.cg_service_list .cg_sl_price{border:1px solid #38383c} ^.im_conversations .imc_history_box .imch_context_menu .imch_cm_btn{color:#f5f5f5;background:#121213;border-bottom:1px solid #38383c} ^.im_conversations .imc_history_box .mbc_btn.__fab{background:#121213;border-color:#38383c} ^.im_conversations .imc_search .search_input{color:#d6d6d6;background:#1b1b1d;border:1px solid #47474c} ^.imc_footer_reply{color:#d8d8d8;background:#121213;border-top:1px solid #3d3d41} ^.imc_tip_before_start .tbs_content{background:#121213;border:2px dashed rgba(74,74,79,0.5)} ^.im_conversations .imch_contact_card{border:2px dashed #4a4a4f} ^.im_typing .imt_dots .imtd_item{background:#3d3d41} ^.pm_activation_2fa .pmaf_title{color:#cacaca} ^.pm_activation_2fa .pmaf_info_wrp{border-top:1px solid #37373b} ^.pm_activation_2fa .pmaf_info{color:#e7e7e7} ^.pm_activation_2fa .pmaf_a_text{color:#c6c6c6} ^.pm_activation_2fa .pmaf_footer{background:#1e1e20} ^.im_conversations .pm_upgraded_membership{color:#f5f5f5;background:#121213;border:2px solid #333336} ^.im_conversations .pm_upgraded_membership.__platinum{border-color:#37373b} ^.im_conversations .pm_upgraded_membership.__platinum .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__platinum .pmu_footer,^.im_conversations .pm_upgraded_membership.__unlimited .pmu_holder.__filled,^.cg_overlay .gc_btn.__white:active{background:#1d1d1f} ^.im_conversations .pm_upgraded_membership.__unlimited{border-color:#313134} ^.im_conversations .pm_upgraded_membership.__sapphire,^.im_conversations .pm_upgraded_membership.__titan{border-color:#323236} ^.im_conversations .pm_upgraded_membership.__sapphire .pmu_highlight{color:#8b52ff} ^.im_conversations .pm_upgraded_membership.__diamond{border-color:#343438} ^.im_conversations .pm_upgraded_membership.__diamond .pmu_highlight{color:#ff527b} ^.im_conversations .pm_upgraded_membership.__exclusive{border-color:#303033} ^.im_conversations .pm_upgraded_membership .pmu_updated_status,^.im_conversations .pm_upgraded_membership .pmu_current_status{color:#94a2bc} ^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper{color:#a079d8} ^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper p span{color:#e17087} ^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper .gift_card_promo_code_ribbon_left:before{border-left:10px solid #2b2b2e} ^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper .gift_card_promo_code_ribbon_right:before{border-right:10px solid #2b2b2e} ^.im_conversations .pm_offering .pmo_bonus{color:#e26f85} ^.im_conversations .pm_downgrade_membership{border:2px solid #303033} ^.im_conversations .pm_downgrade_membership .dmh_content .hc_status{color:#91afbf;background-color:#121213} ^.im_conversations .pm_downgrade_membership .dm_title .dmt_username,^.im_conversations .pm_downgrade_membership .dm_title .dmt_desc{color:#949dbc} ^.im_conversations .pm_downgrade_membership .dm_title .dmt_desc .td_accent{color:#e76978} ^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_card{background:#121213;border:1px solid #3c3c41} ^.smilePanelTabs:before{background:linear-gradient(#29292c,#efefef00)} ^.cg_overlay .cg_options_list .ol_item.__checked{border-color:#3f3f44} ^.cg_overlay .gc_btn{color:#cecece;border:1px solid #3f3f44} ^.cg_overlay .gc_btn.__gray{background:linear-gradient(#151516,#1a1a1c)} ^.cg_overlay .gc_btn.__gray:active{background:#323235} ^.cg_overlay .gc_btn.__disabled{background:#121213;border:1px solid #3f3f44} ^.cg_overlay .cg_prize_list .li_text,^.cg_overlay .cg_prize_list.__wheel .li_text,^.bc_token_keno_field .tkf_cube .fc_body .fc_number{color:#bd9494} ^.cg_overlay .cg_input_wrp .cgi_input_amount{color:#d8d8d8;background:#151516;border:1px solid #4c4c52} ^.cg_overlay .cg_accordion .cga_item:active{background-color:#1d1d1f} ^.cg_overlay .cg_adj_empty_playlist{background-color:#131315;border:1px solid #45454a} ^.cg_overlay .cg_adj_form .djf_link{color:#f5f5f5;border:1px solid #3c3c40} ^.cg_overlay .cg_adj_form .djf_premium.__checked{background-color:#141415} ^.cg_auto_dj_box .adjb_icon,^.cg_overlay .cg_prize_list.__slot .pli_icon_wrp .iw_icon,^.cg_slm_slot .slms_item.__active .slms_icon{fill:#bd9494} ^.cg_overlay .cg_adj_icon_premium .ip_icon{fill:#cfcfcf} ^.cg_overlay .cg_adj_list .pll_item{color:#c1c1c1;background-color:#131315;border-bottom:1px solid #45454a} ^.cg_overlay .cg_game_field .sm_body{background-image:linear-gradient(#1b1b1c 0%,#232326 100%)} ^.cg_overlay .cg_game_field .si_item:after{background:#212123} ^.cg_overlay .cg_game_field .si_item .sii_icon{fill:#f5f5f5} ^.cg_vibe_with_me.__vwm_app .vwm_a_list{color:#f5f5f5;background:#171719} ^.cg_vibe_with_me.__vwm_app .vwm_ai_container:before{background:rgba(43,43,46,0.5)} ^.cg_service_list .cg_sl_item{color:#d8d8d8;border-bottom:1px solid #2e2e31} @media (any-hover:hover) and (pointer:fine){^.mbc_menu .mbcm_item:hover,^.cg_overlay .gc_btn.__white:hover,^.cg_overlay .gc_btn.__gray:hover{background:#1b1b1d} ^.profile_avatar_popup .account_actions .mbc_btn.ac_a_btn_cancel:hover,^.account_actions .mbc_btn.ac_a_btn_cancel:hover{background-color:#1b1b1d} ^.im_conversations .imc_actions .bcf_selectbox:hover{background:#141416} ^.im_conversations .imc_actions .mbc_btn.__btn_msg:hover .imca_text.__new_msg{color:#d6d6d6}} @media screen and (max-width:480px){^.vip-payment .inner-vip .vip-blocks .cont-vip{border-bottom:1px solid #45454a}} @media (max-width:40em){^.notfound p{color:#d8d8d8}} @media (min-width:46em){^.aChat .message .col1{color:#b2b2b2}} @media (max-width:780px){^.im_conversations .pm_upgraded_membership.__gold .pmup_icon,^.im_conversations .pm_upgraded_membership.__sapphire .pmup_icon,^.im_conversations .pm_upgraded_membership.__titan .pmup_icon{background-color:#19191b} ^.im_conversations .pm_upgraded_membership.__platinum .pmup_icon{background-color:#1d1d1f} ^.im_conversations .pm_upgraded_membership.__unlimited .pmup_icon{background-color:#171719} ^.im_conversations .pm_upgraded_membership.__diamond .pmup_icon{background-color:#1b1b1d} ^.im_conversations .pm_upgraded_membership.__exclusive .pmup_icon{background-color:#161618}}`.replaceAll('^', '.bc-dark-m '); const DARK_FIX_CSS = () => `^.bc_fade_scroll:before,^.bc_fade_scroll:after,^.bc_fade_scroll.__listing_tags:before,^.bc_fade_scroll.__listing_tags:after{background:linear-gradient(90deg, #121216, rgba(18,18,22,0))!important} ^.mls_hash_tag{--bc-thm-hashTag-shadow-bg:#121216;--bc-thm-hashTag-shadow-bg-alpha0:rgba(18,18,22,0);--bc-thm-hashTag-viewAllLink-wrp-bg:#121216} ^.bc_notice{--bc-thm-notice-color:#e6e6e6;--bc-thm-notice-bg:linear-gradient(#121213, #121213);--bc-thm-notice-borderColor:linear-gradient(180deg, #3a3a3f, #3a3a3f);--bc-thm-notice-close-iconColor-hover:#dedede;--bc-thm-notice-link-color:#46c2ff;--bc-thm-notice_secondary-paragraph-color:#d8d8d8;--bc-thm-notice_countdownGoal-bg:#18181c;--bc-thm-notice_countdownGoal-borderColor:#8a28eb80;--bc-thm-notice_follow-borderColor:linear-gradient(90deg, #31c4ee, #20b8e3);--bc-thm-notice_pin-borderColor:linear-gradient(90deg, #e75ec9, #d331b0);--bc-thm-notice_love-borderColor:linear-gradient(90deg, #fe4668, #fe254d)} ^.bc_notice.__countdown_goal,^.bc_notice .nt_message{background-color:#18181c;color:#e6e6e6} ^.bc_notice .nt_message,^.bc_notice .nt_message p,^.bc_notice .nt_message span:not(.nt_accent){color:#e6e6e6} ^.chat-tab,^.chat-panel,^.history,^#bChat{--bc-thm-sysMsg_game-resultWrp-bg:#18181c;--bc-thm-sysMsg_game-label-color:#e6e6e6;--bc-thm-sysMsg_game-content-color:#ee5b78;--bc-thm-sysMsg_game-time-color:#a0a0a8;--bc-thm-sysMsg_topicChange-color:#ee5b78;--bc-thm-sysMsg_modelNote-color:#5fd97f;--bc-thm-sysMsg_serviceGroup-bg:#18181c;--bc-thm-sysMsg_serviceGroup-borderColor:#2b2b2e;--bc-thm-sysMsg_serviceGroup-boxShadowColor:#0000;--bc-thm-sysMsg_serviceGroup-boxShadowColor-inset:#2b2b2e;--bc-thm-sysMsg_serviceGroup-desc-color:#bcbcbc;--bc-thm-sysMsg_serviceGroup-title-color:#5fd97f;--bc-thm-sysMsg_friend-bg:#18181c;--bc-thm-sysMsg_friend-borderColor:#3a3a3f;--bc-thm-sysMsg_friend-color:#e6e6e6;--bc-thm-sysMsg_activity-color:#d8d8d8;--bc-thm-sysMsg_activity-matAccent_member-color:#f5f5f5;--bc-thm-sysMsg_service-bg:#2b2b2e;--bc-thm-sysMsg-serviceList-bg:#18181c;--bc-thm-sysMsg-serviceList-authorName-color:#e6e6e6;--bc-thm-sysMsg-serviceList-title-color:#f5f5f5;--bc-thm-sysMsg-serviceList-services-item-title-color:#e6e6e6;--bc-thm-sysMsg-serviceList-services-item-borderColor:#ffffff1f;--bc-thm-sysMsg-serviceList-services-item-bg-hover:#24242a;--bc-thm-sysMsg-serviceList-showBtn-bg-hover:#24242a;--bc-thm-sysMsg-serviceList-showBtn-color-hover:#fff;--bc-thm-game-autoDJ-title-color:#f5f5f5} ^.history .msg_chat_game .cg_msg_wrp,^.history .system_msg.msg_chat_game{background:#18181c!important} ^.history .msg_chat_game .cg_content,^.history .msg_chat_game .gc_msg_label,^.history .msg_chat_game .cg_msg_s_time{background:transparent} ^.history .msg_chat_game .gc_msg_label,^.history .msg_chat_game .gc_msg_label *{color:#e6e6e6} ^.history .msg_chat_game .cg_msg_s_time{color:#a0a0a8} ^.history .msg_chat_game .mbc_btn,^.history .msg_chat_game .bc_btn,^.history .msg_chat_game button,^.history .msg_chat_game a[class*="btn"]{color:#e6e6e6!important;background:transparent!important;border-color:#4c4c52!important} ^.chat_control_panel,^.send-input-container_wrap,^.ccp_inner,^#chat_control_panel{background-color:#121213!important;border-color:#2b2b2e!important;box-shadow:none!important} ^.chat_control_panel:before,^.chat_control_panel:after,^.ccp_inner:before,^.ccp_inner:after,^#bChatInputContainer:before,^#bChatInputContainer:after{border-color:#2b2b2e!important} ^.bc_fade_scroll{--fadeScroll-fade-bg:linear-gradient(90deg, #121216, rgba(18,18,22,0))} ^.bcm_nav_panel,^.bcm_nav_panel.ui-bar-c,^.bcm_nav_panel.ui-footer,^.bcm_nav_panel .np_inner,^.bcm_nav_panel .np_block{background:#18181a!important;background-image:none!important;border-color:#2b2b2e!important} ^.bcm_nav_panel .np_item{color:#d8d8d8!important;background:transparent!important} ^.bcm_nav_panel .np_item.__active,^.bcm_nav_panel .np_item:active{color:#e36e84!important} ^.bc_fade_scroll.__listing_tags{--fadeScroll-fade-bg:linear-gradient(90deg, #121216, rgba(18,18,22,0))} ^.profile_info_lg{--profileInfo-color:#e6e6e6;--profileInfo-base-accentTitle-color:#ee5b78;--profileInfo-section-bg:#1b1b1f;--profileInfo-detailsWrp-bg:#27272c;--profileInfo-detailsMore-color:#e6e6e6;--profileInfo-flag-borderColor:#3a3a3f;--profileInfo-shelf-broderColor:#3a3a3f;--profileInfo-shelf-badge-bg:#27272c;--profileInfo-section_shedule-text_highlight-color:#ee5b78;--profileInfo-qoqContests-chat-bg:#1b1b1f;--profileInfo-qoqContests-title-color:#ee5b78;--profileInfo-qoqContests-text-color:#e6e6e6;--profileInfo-gifts-emptyHead-imgWrp-bg:#27272c;--profileInfo-gifts-emptyHead-imgWrp-borderColor:#3a3a3f;--profileInfo-comments-content-bg:#27272c;--profileInfo-comments-content-text-color:#e6e6e6;--profileInfo-comments-btn-color:#e6e6e6;--profileInfo-comments-btn-bg:#27272c;--profileInfo-section_tags-stlItem-color:#e6e6e6;--profileInfo-section_tags-stlItem-bg:#27272c;--profileInfo_section_wish-listItem-bg:#27272c;--profileInfo-listWrp-bg:#27272c;--profileInfo-listItem-bg:#1b1b1f;--profileInfo-listItemCaption-color:#9a9aa2;--profileInfo-listItemHighlight-color:#e6e6e6}`.replaceAll('^', '.bc-dark-m '); const DARK_ALL_CSS = () => `^.bc_notice,^.bc_notice.__countdown_goal,^.bc_notice.__primary,^.bc_notice.__secondary{--bcm-notice-color:#e6e6e6;--bcm-notice-bg:linear-gradient(#121213, #121213);--bcm-notice-borderColor:linear-gradient(#2e2e33, #2e2e33);--bcm-notice-link-color:#46c2ff;--bcm-notice-close-iconColor:#a0a0a8;--bcm-notice_countdownGoal-bg:linear-gradient(#121213, #121213);--bcm-notice_countdownGoal-borderColor:linear-gradient(#2e2e33, #2e2e33);--bcm-notice_membershipError-bg:linear-gradient(#121213, #121213);--bcm-notice_membershipError-borderColor:linear-gradient(#2e2e33, #2e2e33);--bcm-notice_notEnoughtTokens-bg:linear-gradient(#121213, #121213);--bc-thm-notice_topic-bg:#18181c;--bc-thm-notice_topic-color:#ee5b78;--bc-thm-notice_attention-bg:#1d1828;--bc-thm-notice_attention-color:#c291f2} ^.bc_notice.__countdown_goal,^.bc_notice.__primary{background:linear-gradient(#121213, #121213) padding-box, linear-gradient(#2e2e33, #2e2e33) border-box!important;border-color:transparent!important;box-shadow:none!important} ^.bc_notice.__countdown_goal .nt_message,^.bc_notice.__countdown_goal .nt_message *:not(.nt_accent):not(.nt_tip_btn){color:#e6e6e6} ^.mls_hash_tag{--mlsHashTag-tag-color:#e6e6e6;--mlsHashTag-tag-bg:#26262b;--mlsHashTag-tag-bg-active:#34343a;--mlsHashTag-tag_record-icon-color:#a0a0a8;--mlsHashTag-viewAllLink-color:#46c2ff;--mlsHashTag-removeFilterLink-close-iconColor:#c4c4c4} ^.mls_hash_tag .ht_item,^.mls_hash_tag .ht_item.ui-link{background:#26262b!important;color:#e6e6e6!important} ^.mls_tag_badge{--bcm-listing-tagBadge-bg:#26262b;--bcm-listing-tagBadge-borderColor:#3a3a3f;--bcm-listing-tagBadge-color:#e6e6e6;--bcm-listing-tagBadge-button-bg:#4c4c52;--bcm-listing-tagBadge-button-iconColor:#fff} ^.bcm_popup,^.filter_popup,^.search_popup,^.mbc_search_block{--bcm-popup-body-bg-color:#121213;--bcm-popup-filter-main-bg:#121213;--bcm-popup-filter-features-title-color:#c1c1c1;--bcm-popup-filter-footer-borderColor:#2e2e33;--bcm-popup-filter-notice-color:#c1c1c1;--bcm-popup-filter-button-outline-color:#d8d8d8;--bcm-popup-filter-button-outline-color-active:#fff;--bcm-popup-filter-button-outline-bg:#1b1b1f;--bcm-popup-filter-button-outline-borderColor:#3e3e43;--bcm-popup-search-header-bg-color:#18181a;--bcm-search-block-input-bg-color:#1b1b1f;--bcm-search-block-input-borderColor:#3e3e43;--bcm-search-block-input-placeholder-color:#8e8e96;--bcm-search-block-header-close-icon-color:#d8d8d8;--bcm-search-block-reset-button-bg-color:#26262b;--bcm-search-block-reset-button-border-color:#3e3e43;--bcm-search-block-result-name-color:#e6e6e6;--bcm-search-block-result-border-color:#2b2b2e;--bcm-search-block-result-tag-color:#e6e6e6;--bcm-search-block-result-tag-name-bg-color:#26262b} ^.bcm_popup .bcm_popup_body,^.bcm_popup .bp_body,^.filter_popup .fp_main,^.filter_popup .fp_body,^.filter_popup .fp_footer,^.filter_popup .fp_content,^.search_popup .sp_body,^.mbc_search_block{background-color:#121213!important;color:#d8d8d8} ^.bcm_range{--bcm-range-values-color:#bcbcbc;--bcm-range-track-bg-color:#2b2b2e;--bcm-range-handle-bg-color:#d8d8d8;--bcm-range-handle-border-color:#4c4c52} ^.bcm_accordion{--bcm-accordion-box-shadow-color:#2b2b2e;--bcm-accordion-title-color:#bcbcbc;--bcm-accordion-arrow-fill:#d8d8d8} ^.bcm_form_alert{--bcm-form-alert-error-bg-color:#2a1a1d;--bcm-form-alert-error-color:#ff7a85;--bcm-form-alert-success-bg-color:#173320;--bcm-form-alert-success-color:#b6f0c4} ^.bcm_switcher{--bcm-switcher-bg:#3a3a3f} ^.bcm_socials{--bcm-thm-socials-counter-color:#bcbcbc;--bcm-thm-socials-counter-bg:#18181c;--bcm-thm-socials-counter-borderColor:#3a3a3f} ^.bcm_footer{--bcm-thm-footer-bg:#18181a;--bcm-thm-footer-borderColor:#2b2b2e;--bcm-thm-footer-details-color:#d8d8d8;--bcm-thm-footer-details-nav-link-color:#d8d8d8;--bcm-thm-footer-details-nav-link-bg-active:#26262b;--bcm-thm-copyright-bg:#121213;--bcm-thm-copyright-color:#bcbcbc} ^.bcm_chat_footer{--bcm-chat-footer-item-border-color:#2b2b2e} ^.bcm_history_scroll_up{--bcm-historyScrollUp-bg:#26262b} ^.bcm_chat_auto_message{--bcm-chat-member-notification-borderColor:#3a3a3f;--bcm-chat-member-notification-title-color:#e6e6e6} ^input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=reset]):not([type=file]):not([type=color]):not([type=image]):not([type=hidden]),^textarea,^.mbc_search_block .sb_input{color:#f0f0f0!important;-webkit-text-fill-color:#f0f0f0!important;caret-color:#f0f0f0!important}`.replaceAll('^', '.bc-dark-m '); const darkOpaqueCss = () => '.panel,.bm-modal{background:' + 'rgb(18,18,22)' + '!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}'; const syncOpaque = on => { const sr = document.getElementById('bc-unchained-host')?.shadowRoot; const st = sr?.getElementById('bc-dark-opaque'); if (!sr || on === !!st) return; if (st) return st.remove(); const e = document.createElement('style'); e.id = 'bc-dark-opaque'; e.textContent = darkOpaqueCss(); sr.appendChild(e); }; const setCls = (el, c, on) => { if (el.classList.contains(c) !== on) el.classList.toggle(c, on); }; const addStyle = (id, css, root) => { if (document.getElementById(id)) return; const st = document.createElement('style'); st.id = id; st.textContent = typeof css === 'function' ? css() : css; (document.head || root).appendChild(st); }; const darkBcDesktop = (b, on) => { const cl = b.classList; if (on) { if (b.dataset.bcTheme === undefined && !cl.contains('theme_dark')) b.dataset.bcTheme = cl.contains('theme_light') ? '1' : '0'; setCls(b, 'theme_light', false); setCls(b, 'theme_dark', true); } else if (b.dataset.bcTheme !== undefined) { const wasLight = b.dataset.bcTheme === '1'; delete b.dataset.bcTheme; setCls(b, 'theme_dark', false); if (wasLight) setCls(b, 'theme_light', true); } }; const isOwnNode = n => n.nodeType === 1 && (n.classList.contains('bcu-star') || n.classList.contains('bcu-ring')); const onlyOwnNodes = recs => { for (const r of recs) { if (r.type !== 'childList') return false; for (const n of r.addedNodes) if (!isOwnNode(n)) return false; for (const n of r.removedNodes) if (!isOwnNode(n)) return false; } return true; }; const DomBus = (() => { const subs = []; let mo = null; const start = () => { if (mo || !document.documentElement) return; mo = new MutationObserver(recs => { let own; const isOwn = () => own === undefined ? (own = onlyOwnNodes(recs)) : own; for (const fn of subs) { try { fn(recs, isOwn); } catch (e) { dbg(e, 'start'); } } }); mo.observe(document.documentElement, { childList: true, subtree: true }); }; return { subscribe(fn) { subs.push(fn); start(); } }; })(); const DarkSweep = (() => { const SKIP = '.video-panel-wrap,video,canvas,svg,img,#bc-unchained-host'; const INK = '#e6e6e6'; const BORDERS = [['Top', 0.55], ['Bottom', 0.55], ['Left', 0.75], ['Right', 0.75]]; let on = false, mo = null, tm = 0, seen = new WeakSet(), gen = 0, lastSweep = 0; const touched = []; let touchedCap = 4000; const pruneTouched = () => { const live = touched.filter(r => r.deref()); touched.length = 0; for (const r of live) touched.push(r); touchedCap = Math.max(4000, live.length * 2); }; const rgba = s => s.match(/[\d.]+/g)?.map(Number) ?? null; const solid = (c, a) => !!c && (c.length < 4 || c[3] > a); const lumOf = c => (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255; const spread = c => Math.max(c[0], c[1], c[2]) - Math.min(c[0], c[1], c[2]); const darkInk = c => solid(c, 0.3) && lumOf(c) < 0.33; const styleOf = el => { try { return getComputedStyle(el); } catch { return null; } }; const put = (el, prop, val) => { if (!el.__bcdm) { el.__bcdm = {}; touched.push(new WeakRef(el)); if (touched.length > touchedCap) pruneTouched(); } if (!(prop in el.__bcdm)) el.__bcdm[prop] = el.style.getPropertyValue(prop); if (defer) defer.push(el, prop, val); else el.style.setProperty(prop, val, 'important'); }; let defer = null; const batched = fn => { if (defer) return fn(); defer = []; try { fn(); } finally { const d = defer; defer = null; for (let i = 0; i < d.length; i += 3) d[i].style.setProperty(d[i + 1], d[i + 2], 'important'); } }; const REJECT_SEL = '.video-panel-wrap,video,canvas,img,#bc-unchained-host'; const walkFilter = { acceptNode: n => (n.ownerSVGElement || n.matches(REJECT_SEL)) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT }; const walk = (root, cb) => { const tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, walkFilter); for (let n = tw.nextNode(); n; n = tw.nextNode()) cb(n); }; const fixSvg = el => { const cs = styleOf(el); if (!cs) return; if (darkInk(rgba(cs.color))) put(el, 'color', INK); for (const k of ['fill', 'stroke']) { const v = cs[k]; if (v && v !== 'none' && !v.startsWith('url') && darkInk(rgba(v))) put(el, k, INK); } }; const hasText = el => Array.from(el.childNodes).some(n => n.nodeType === 3 && n.nodeValue.trim()); const nestedBg = el => { for (let a = el.parentElement; a; a = a.parentElement) { if (a.__bcdm && 'background-color' in a.__bcdm) return true; if (a.id === 'bChatContent') break; } return false; }; const fixBg = (el, cs) => { const bg = rgba(cs.backgroundColor); if (solid(bg, 0.6) && lumOf(bg) > 0.88) put(el, 'background-color', nestedBg(el) ? '#27272c' : '#1b1b1f'); }; const fixBorders = (el, cs) => { for (const [side, lim] of BORDERS) { const c = rgba(cs[`border${side}Color`]); if (parseFloat(cs[`border${side}Width`]) > 0 && solid(c, 0.3) && lumOf(c) > lim && spread(c) < 40) put(el, `border-${side.toLowerCase()}-color`, '#3a3a3f'); } }; const isRed = c => c[0] > 100 && c[1] < 90 && c[2] < 100 && c[0] > c[1] * 1.8; const textTone = c => { const L = lumOf(c), sat = spread(c); if (isRed(c)) return L < 0.33 ? '#ee5b78' : null; return (sat < 40 && L < 0.45) || (L < 0.33 && sat < 90) ? INK : null; }; const fixText = (el, cs) => { const c = hasText(el) && rgba(cs.color); const tone = c && textTone(c); if (tone) put(el, 'color', tone); }; const fix = el => { const cs = styleOf(el); if (!cs) return; fixBg(el, cs); fixBorders(el, cs); fixText(el, cs); }; const inBox = el => !!el.closest && !el.closest('#bc-unchained-host'); const visit = (el, trusted) => { if (el.nodeType !== 1 || seen.has(el)) return; seen.add(el); if (el.localName === 'svg') { if (trusted || !el.parentElement?.closest(SKIP)) fixSvg(el); } else if (trusted || !el.closest(SKIP)) fix(el); }; const visitTree = el => { if (el.nodeType !== 1 || !inBox(el)) return; visit(el); if (el.localName === 'svg' || el.ownerSVGElement || el.closest(SKIP)) return; walk(el, n => visit(n, true)); }; const inViewport = e => { const r = e.getBoundingClientRect(), vh = window.innerHeight + 80, vw = window.innerWidth; return r.width > 0 && r.height > 0 && r.bottom > -80 && r.top < vh && r.right > 0 && r.left < vw; }; const sweep = () => { tm = 0; const box = document.body; if (!on || !box) return; lastSweep = Date.now(); const my = ++gen; const first = [], list = []; walk(box, e => { if (!seen.has(e)) (inViewport(e) ? first : list).push(e); }); batched(() => { for (const e of first) visit(e, true); }); let i = 0; const step = () => { if (!on || my !== gen) return; const t0 = performance.now(); if (i < list.length) batched(() => { do visit(list[i++], true); while (i < list.length && performance.now() - t0 < 8); }); if (i < list.length) requestAnimationFrame(step); }; step(); }; const pendAttr = new Set(), pendTree = new Set(); let pendRaf = 0, pendTo = 0; const flushPend = () => { cancelAnimationFrame(pendRaf); clearTimeout(pendTo); pendRaf = pendTo = 0; const trees = [...pendTree], attrs = [...pendAttr]; pendTree.clear(); pendAttr.clear(); if (!on) return; batched(() => { for (const n of trees) if (n.isConnected) visitTree(n); for (const t of attrs) if (t.isConnected) fix(t); }); }; const schedPend = () => { if (pendRaf || pendTo) return; pendRaf = requestAnimationFrame(flushPend); pendTo = setTimeout(flushPend, 250); }; const onAttr = ({ target: t, attributeName }) => { if (t.nodeType !== 1 || !inBox(t) || t.closest(SKIP)) return; if (attributeName === 'style' && (t.__bcdm || !t.closest('#bChatContent'))) return; pendAttr.add(t); }; const onMut = recs => { if (!on) return; for (const r of recs) { if (r.type === 'childList') r.addedNodes.forEach(n => { if (!isOwnNode(n)) pendTree.add(n); }); else if (r.type === 'attributes') onAttr(r); } if (pendTree.size || pendAttr.size) schedPend(); }; const queue = () => { if (!tm) tm = requestAnimationFrame(sweep); }; const revert = () => { for (const ref of touched) { const el = ref.deref(); if (!el || !el.__bcdm) continue; for (const k in el.__bcdm) { const v = el.__bcdm[k]; v ? el.style.setProperty(k, v) : el.style.removeProperty(k); } delete el.__bcdm; } touched.length = 0; seen = new WeakSet(); }; const start = () => { if (!mo && document.body) { mo = new MutationObserver(onMut); mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'hidden', 'style'] }); } queue(); }; const stop = () => { gen++; mo?.disconnect(); mo = null; pendTree.clear(); pendAttr.clear(); revert(); }; return { set(v) { v = !!v; if (v === on) { if (on && Date.now() - lastSweep > 10000) queue(); return; } on = v; on ? start() : stop(); } }; })(); const markHostDfb = on => { try { document.getElementById('bc-unchained-host')?.toggleAttribute('data-dfb', !!on); } catch (e) { dbg(e, 'markHostDfb'); } }; const DARK_M_SHEETS = [['bc-unchained-dark-m', DARK_M_CSS], ['bc-unchained-dark-fix', DARK_FIX_CSS], ['bc-unchained-dark-all', DARK_ALL_CSS]]; const darkBcMobile = (root, on) => { DarkSweep.set(on); markHostDfb(false); if (on) DARK_M_SHEETS.forEach(([id, css]) => addStyle(id, css, root)); setCls(root, 'bc-dark-m', on); setCls(root, 'bc-dark-fb', false); syncOpaque(false); }; const darkGeneric = (root, on) => { if (on) addStyle('bc-unchained-dark-css', DARK_FB_CSS, root); if (on) addStyle('bc-unchained-dark-css-wk', DARK_FB_CSS_WK, root); setCls(root, 'bc-dark-fb', on); markHostDfb(on); syncOpaque(on); }; const PLAT_UA = /Android|iPhone|iPad|iPod|Mobile|Opera Mini|IEMobile/i; let platFinal = false; const platGuess = () => { const h = Store.get('bc_plat', null); return h === 'm' || h === 'd' ? h : (PLAT_UA.test(navigator.userAgent) ? 'm' : 'd'); }; const platNow = () => (document.body?.classList.contains('ui-mobile-viewport') ? 'm' : platFinal ? 'd' : platGuess()); const rememberPlat = p => { if (Store.get('bc_plat', null) !== p) Store.set('bc_plat', p); }; const applyDark = () => { const b = document.body, root = document.documentElement; if (!b || !root) return; const on = AppState.dark; if (!BC_RE.test(location.hostname)) return darkGeneric(root, on); const p = platNow(); if (platFinal || b.classList.contains('ui-mobile-viewport')) rememberPlat(p); if (p === 'd') { if (root.classList.contains('bc-dark-m')) darkBcMobile(root, false); darkBcDesktop(b, on); } else darkBcMobile(root, on); }; (() => { const start = () => { try { applyDark(); } catch (e) { dbg(e, 'start'); } let q = 0; const mo = new MutationObserver(() => { if (!AppState.dark || q) return; q = requestAnimationFrame(() => { q = 0; if (AppState.dark) try { applyDark(); } catch (e) { dbg(e, 'start'); } }); }); mo.observe(document.body, { attributes: true, attributeFilter: ['class'] }); mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); const settle = () => { platFinal = true; try { applyDark(); } catch (e) { dbg(e, 'start.settle'); } }; if (document.readyState === 'complete') settle(); else window.addEventListener('load', settle, { once: true }); }; if (document.body) return start(); const mo = new MutationObserver(() => { if (document.body) { mo.disconnect(); start(); } }); mo.observe(document.documentElement, { childList: true }); })(); const REC_RES = { hi: '1080p', mid: '720p', low: '480p', min: '360p' }; const REC_Q = { hi: [6000000, 128000], mid: [3500000, 128000], low: [2000000, 96000], min: [1000000, 64000] }; const REC_PART = [50, 100, 150, 300]; const REC_TOTAL = [0, 512, 1024, 2048, 4096]; const REC_H = { hi: Infinity, mid: 720, low: 480, min: 360 }; const recNorm = c => { c = (c && typeof c === 'object' && !Array.isArray(c)) ? c : {}; return { q: REC_Q[c.q] ? c.q : 'hi', part: REC_PART.includes(c.part) ? c.part : 150, total: REC_TOTAL.includes(c.total) ? c.total : 0, fmt: c.fmt === 'webm' ? 'webm' : 'mp4', src: c.src === 'cap' ? 'cap' : 'hls' }; }; const recCfg = () => recNorm(Store.get('bc_rec', {})); const fmtSize = (b, mb, gb) => { const m = b / 1048576; return m >= 1024 ? `${(m / 1024).toFixed(2)}${gb}` : `${m.toFixed(1)}${mb}`; }; const fmtShort = b => fmtSize(b, 'M', 'G'); const fmtBytes = b => { const l = L(); return fmtSize(b, ` ${l.u_mb}`, ` ${l.u_gb}`); }; const BK_CHECK = { cols: v => Number.isInteger(v) && v >= 1 && v <= 2, b: v => typeof v === 'boolean', scale: v => typeof v === 'number' && v >= 0.6 && v <= 1.5, css: v => typeof v === 'string' && /^-?\d+(\.\d+)?(px|%)$/.test(v), color: v => typeof v === 'string' && v.length < 60 && /^(rgba?|hsla?)\([\d\s.,%]+\)$/.test(v), lang: v => LANGS.includes(v), sort: v => typeof v === 'string' && /^[a-z]{1,12}$/.test(v), spd: v => v === 0 || v === 500 || v === 1000, wf: v => v === -1 || v === 0 || v === 1, lay: v => Number.isInteger(v) && v >= 0 && v <= 3, ctext: v => typeof v === 'string' && v.length <= 80, keys: v => Array.isArray(v) && v.length <= 2000 && v.every(x => typeof x === 'string' && x.length <= 160), hsort: v => v === 'last' || v === 'time' || v === 'visits', mvdvr: v => Array.isArray(MV_DVR_OPTS) && MV_DVR_OPTS.includes(v), vol: v => typeof v === 'number' && v >= 0 && v <= 1, cbmode: v => v === 'off' || v === 'anim' || v === 'video', cbh: v => v === 240 || v === 360 || v === 480 || v === 720 }; const BACKUP_SCHEMA = { bc_bg: 'b', bc_min: 'b', bc_hid: 'b', bc_pos: 'b', bc_gmute: 'b', bc_dark: 'b', bc_bm_gal: 'b', bc_bm_hide_topic: 'b', bc_bm_hide_grp: 'b', bc_bm_hide_ptags: 'b', bc_bm_hide_note: 'b', bc_bm_hide_priv: 'b', bc_bm_hide_group: 'b', bc_bm_hide_vip: 'b', bc_bm_hide_away: 'b', bc_bm_hide_off: 'b', bc_bm_sort_rev: 'b', bc_previews: 'b', bc_favnav: 'b', bc_scale: 'scale', bc_x: 'css', bc_y: 'css', bc_fx: 'css', bc_fy: 'css', bc_fb: 'css', bc_theme: 'color', bc_lang: 'lang', bc_bm_sort_type: 'sort', bc_prev_spd: 'spd', bc_hide_cfg: 'hcfg', bc_hide_custom: 'hcustom', bc_rec: 'rec', bc_lang_auto: 'b', bc_hist: 'hist', bc_hist_ch: 'b', bc_btnhide: 'btns', bc_star: 'b', bc_hist_on: 'b', bc_bm_cols_g: 'cols', bc_bm_cols_l: 'cols', bc_bm_wf: 'wf', bc_cal_wf: 'wf', bc_cal_lay: 'lay', bc_cal_grp: 'ctext', bc_cal_tag: 'ctext', bc_cal_models: 'keys', bc_hist_sort: 'hsort', bc_mv_auto: 'b', bc_mv_bg: 'b', bc_mv_hide_off: 'b', bc_mv_hide_priv: 'b', bc_mv_hide_group: 'b', bc_mv_hide_vip: 'b', bc_mv_hide_away: 'b', bc_rad: 'b', bc_rad_x: 'css', bc_rad_y: 'css', bc_mv_abr: 'b', bc_mv_dvr: 'mvdvr', bc_mv_vol: 'vol', bc_mv: 'mvlist', cbhp_mode: 'cbmode', cbhp_sound: 'b', cbhp_maxh: 'cbh' }; const safeSel = x => { if (typeof x !== 'string' || !x.trim() || x.length > 400 || /[{};@]|\/\*/.test(x)) return false; try { document.querySelector(x); return true; } catch { return false; } }; const cleanHideCfg = o => { if (!o || typeof o !== 'object' || Array.isArray(o)) return null; const out = {}; HIDE_CATS.forEach(c => { if (typeof o[c.id] === 'boolean') out[c.id] = o[c.id]; }); return out; }; const cleanHideCustom = o => { if (!o || typeof o !== 'object' || Array.isArray(o)) return null; const out = {}; Object.keys(o).forEach(k => { if (!/^[a-z0-9.-]{1,40}$/i.test(k) || !Array.isArray(o[k])) return; out[k] = o[k].filter(safeSel).slice(0, 200); }); return out; }; const histNum = v => Number.isFinite(v) && v >= 0 ? v : 0; const cleanHist = o => { if (!o || typeof o !== 'object' || Array.isArray(o)) return null; const m = {}, list = []; const src = o.m && typeof o.m === 'object' && !Array.isArray(o.m) ? o.m : {}; Object.keys(src).forEach(k => { const e = src[k]; if (!/^[a-z0-9.-]{1,40}\|.{1,120}$/i.test(k) || !e || typeof e !== 'object' || !isStr(e.u) || !isSafeUrl(e.u)) return; m[k] = { n: clip(e.n, 100), u: normUrl(e.u), a: toHttps(e.a), s: histNum(e.s), v: histNum(e.v), f: histNum(e.f), l: histNum(e.l) }; }); (Array.isArray(o.s) ? o.s : []).forEach(x => { if (x && typeof x === 'object' && isStr(x.k) && m[x.k]) list.push({ k: x.k, n: clip(x.n, 100), t: histNum(x.t), d: histNum(x.d) }); }); return { m, s: list }; }; const mergeHist = (a, b) => { const cur = cleanHist(a) || { m: {}, s: [] }, inc = cleanHist(b) || { m: {}, s: [] }; Object.keys(inc.m).forEach(k => { if (!cur.m[k] || cur.m[k].s < inc.m[k].s) cur.m[k] = inc.m[k]; }); const seen = new Set(cur.s.map(x => `${x.k}|${x.t}`)); inc.s.forEach(x => { if (cur.m[x.k] && !seen.has(`${x.k}|${x.t}`)) cur.s.push(x); }); cur.s.sort((x, y) => x.t - y.t); return cur; }; const cleanBtns = o => { if (!o || typeof o !== 'object' || Array.isArray(o)) return null; const out = {}; Object.keys(o).forEach(k => { if (/^[A-Za-z]{2,12}$/.test(k) && o[k] === true) out[k] = true; }); return out; }; const cleanMvList = a => Array.isArray(a) ? a.filter(e => isObj(e) && (e.t === 'bm' || e.t === 'url') && isStr(e.url) && e.url.length <= 600 && isSafeUrl(e.url)).slice(0, MV_MAX).map(e => ({ t: e.t, name: clip(e.name, 100), url: e.url })) : null; const BK_CLEAN = { mvlist: cleanMvList, hist: cleanHist, btns: cleanBtns, hcfg: cleanHideCfg, hcustom: cleanHideCustom, rec: o => (o && typeof o === 'object' && !Array.isArray(o)) ? recNorm(o) : null }; const bkValue = (kind, v) => BK_CLEAN[kind] ? BK_CLEAN[kind](v) : (BK_CHECK[kind](v) ? v : null); const restoreSettings = src => Object.entries(BACKUP_SCHEMA).reduce((n, [k, kind]) => { if (!Object.prototype.hasOwnProperty.call(src, k)) return n; const v = bkValue(kind, src[k]); if (v == null) return n; Store.set(k, k === 'bc_hist' ? mergeHist(Store.get('bc_hist', {}), v) : v); return n + 1; }, 0); const Backup = { parts() { const p = Store.get('bc_bk_parts', {}); const o = { s: p.s !== false, f: p.f !== false, h: p.h !== false }; return o.s || o.f || o.h ? o : { s: true, f: true, h: true }; }, setPart(k, on) { const p = this.parts(); p[k] = on; if (!p.s && !p.f && !p.h) return false; Store.set('bc_bk_parts', p); return true; }, build() { try { syncBookmarks(); } catch (e) { dbg(e, 'build'); } const p = this.parts(); const out = { app: 'Cams Unchained', ver: 2, date: new Date().toISOString() }; if (p.s) { const settings = {}; Object.keys(BACKUP_SCHEMA).forEach(k => { if (k === 'bc_hist') return; const v = Store.get(k, null); if (v !== null) settings[k] = v; }); out.settings = settings; } if (p.f) out.bookmarks = AppState.bookmarks.map(stripBm); if (p.h) out.history = histLoad(); return out; }, export() { const blob = new Blob([JSON.stringify(this.build(), null, 2)], { type: 'application/json' }); Helpers.download(blob, `BC_Backup_${Helpers.getTimestamp().slice(0, 10)}.json`); }, exportFavCsv() { const rows = [['name', 'url', 'group', 'note', 'pinned', 'last_seen'], ...AppState.bookmarks.map(b => [b.name, b.url, b.g || '', b.note || '', b.pin ? 1 : 0, b.seen ? fmtStamp(b.seen) : ''])]; Helpers.download(new Blob([csvText(rows)], { type: 'text/csv;charset=utf-8' }), `BC_Favorites_${Helpers.getTimestamp().slice(0, 10)}.csv`); }, apply(data) { const p = this.parts(); const favs = list => (p.f && Array.isArray(list) ? mergeBookmarks(list) : 0); if (Array.isArray(data)) return { n: 0, added: favs(data) }; if (data?.app !== 'Cams Unchained' && data?.app !== 'BongaCams Unchained') throw new Error('bad backup'); const n = p.s ? restoreSettings(isObj(data.settings) ? data.settings : {}) : 0; const added = favs(data.bookmarks); if (p.h && isObj(data.history)) { const hc = cleanHist(data.history); if (hc) histMerge(hc); } return { n, added }; }, import(onOk, onErr) { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.style.display = 'none'; (document.body || document.documentElement).appendChild(input); input.oncancel = () => input.remove(); input.onchange = async e => { try { const file = e.target.files?.[0]; if (!file) return; if (file.size > 3e7) throw new Error('too big'); onOk(this.apply(JSON.parse(await file.text()))); } catch { onErr(); } finally { input.remove(); } }; input.click(); } }; const LANG_SEG = /^(?:ru|uk|ua|en|es|de|fr|pt|it|pl|tr|nl|cs|ro|hu|bg|el|ja|zh|ko|he|ar|hi|id|vi|th)$/i; const bcSlug = seg => { while (seg.length > 1 && LANG_SEG.test(seg[0])) seg.shift(); if (/^profile$/i.test(seg[0] || '')) seg.shift(); const s = seg[0] || ''; return BC_SYS.test(s) ? '' : s.toLowerCase(); }; const plainSlug = (sys, valid) => seg => { const s = seg[0] || ''; return sys.test(s) || !valid.test(s) ? '' : s; }; const SLUG_BY_SITE = new Map([['bc', bcSlug], ['sc', plainSlug(SC_SYS, /^[\w.-]+$/)], ['cb', plainSlug(CB_SYS, /^\w+$/)]]); const firstSeg = seg => seg[0] || ''; const modelSlug = u => { try { const x = new URL(u); const seg = x.pathname.split('/').filter(Boolean).map(safeDecode); return (SLUG_BY_SITE.get(siteKey(x.hostname)) || firstSeg)(seg); } catch { return ''; } }; const bmKeyRaw = u => { const s = modelSlug(u); return s ? `${urlSite(u)}|${s.toLowerCase()}` : String(u); }; const bmKeyCache = new Map(); const bmKey = u => { if (typeof u !== 'string') return bmKeyRaw(u); let k = bmKeyCache.get(u); if (k === undefined) { if (bmKeyCache.size > 3000) bmKeyCache.clear(); k = bmKeyRaw(u); bmKeyCache.set(u, k); } return k; }; const pageKey = () => { const s = modelSlug(location.href); return s ? `${SITE}|${s.toLowerCase()}` : ''; }; const normUrl = (u, host) => { try { if (!isSafeUrl(u)) return u; const x = new URL(u), s = modelSlug(u); return s ? `https://${host || x.host}/${encodeURIComponent(s)}` : x.href; } catch { return u; } }; const linkFor = u => { try { const host = urlSite(u) === siteKey(location.hostname) ? location.host : new URL(u).host; return normUrl(u, host); } catch { return u; } }; const isFavKey = k => !!k && AppState.bookmarks.some(b => bmKey(b.url) === k); const isFavName = name => !!name && (AppState.bookmarks.some(b => sameBm(b, name)) || isFavKey(pageKey())); const BM_FILL_KEYS = ['note', 'g', 'avatar']; const absorbBm = (prev, b) => { BM_FILL_KEYS.forEach(k => { if (!prev[k] && b[k]) prev[k] = b[k]; }); if (b.pin) prev.pin = true; prev.seen = Math.max(prev.seen || 0, b.seen || 0); }; const migrateBookmarks = () => { const map = new Map(); const before = AppState.bookmarks.length; let changed = false; AppState.bookmarks.forEach((b, i) => { if (!b || !isStr(b.url)) { map.set(`#${i}`, b); return; } const nu = normUrl(b.url); if (nu !== b.url) { b.url = nu; changed = true; } const k = bmKey(b.url), prev = map.get(k); if (!prev) { map.set(k, b); return; } changed = true; absorbBm(prev, b); }); if (changed) { AppState.bookmarks = [...map.values()].filter(Boolean); saveBookmarks(); } return before - AppState.bookmarks.length; }; migrateBookmarks(); const readFb = () => { const fb = Store.get('bc_fb', null); if (fb) return fb; const fy = Store.get('bc_fy', null); if (!fy || window.innerHeight < 200) return null; const v = `${Math.max(0, Math.round(window.innerHeight - parseFloat(fy) - 50))}px`; Store.set('bc_fb', v); return v; }; const injectSiteCSS = () => { if (document.getElementById('bc-unchained-site-css')) return; const fx = Store.get('bc_fx', null), fb = readFb(); let css = `${currentSite.css || ''} .js-filter_button, .bc_sticky_bar { touch-action: none !important; user-select: none !important; -webkit-user-drag: none !important; } .bc_sticky_bar .bc_filter_button,.bc_sticky_bar .js-filter_button{animation:none!important;transition:none!important;transform:none!important;translate:none!important;opacity:1!important;top:auto!important;bottom:auto!important;inset-block:auto!important} html.bc-theater-html{background:#000!important;overflow:hidden!important} body.bc-theater-mode{overflow:hidden!important;background:#000!important;margin:0!important;padding:0!important} body.bc-theater-mode :is(.chat_panel_wrap,.player_footer,[class*="chat-panel"],[class*="chat_panel"],[class*="player-footer"],#chat_bar_v2_container,#chat_bar_v2,#switch_between_chat,.chat-tabs,[class*="chat_nav"],[class*="chat_bar"],#games_button,.mls_title,.live_tabs,.page_header,.header_bar,.chat-wrapper,.bc_chat_header,.memberBalance,.memberNavigation,.private_btn_wrp,#bPaidButtonTopRow,#profile-content,.stream-controls,#top_banner_above_chat,[class*="footer"]:not(#bc-unchained-host *)){display:none!important} body.bc-theater-mode .bc-thtr-anc{transform:none!important;translate:none!important;scale:none!important;rotate:none!important;filter:none!important;backdrop-filter:none!important;perspective:none!important;contain:none!important;overflow:visible!important;clip-path:none!important;mask:none!important;will-change:auto!important} body.bc-theater-mode .bc-thtr-wrap{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;width:100dvw!important;height:100dvh!important;max-width:none!important;max-height:none!important;z-index:2147483644!important;background:#000!important;margin:0!important;padding:0!important;transform:none!important;display:block!important} body.bc-theater-mode video.bc-thtr-video{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;width:100dvw!important;height:100dvh!important;max-width:none!important;max-height:none!important;aspect-ratio:auto!important;z-index:2147483646!important;object-fit:contain!important;background:#000!important;margin:0!important;padding:0!important;transform:none!important;translate:none!important;rotate:none!important;scale:none!important}`; if (fx && fb) { css += `.bc_sticky_bar:not([data-bcu]){visibility:hidden!important}.bc_sticky_bar{left:${fx}!important;top:auto!important;bottom:${fb}!important;transform:none!important;margin:0!important;right:auto!important;width:max-content!important;transition:none!important;animation:none!important}`; } addStyle('bc-unchained-site-css', css, document.documentElement); }; injectSiteCSS(); applyHideCSS(); const LOCK_SEL = '.fp_outer,.fp_outer_overlay,.fp_inner_wrap'; const unlockScroll = () => { if (!BC_RE.test(location.hostname) || !document.documentElement) return; const root = document.documentElement; const locked = !!document.querySelector(LOCK_SEL); if (!locked) { root.classList.remove('bc-unlock'); return; } addStyle('bc-unchained-unlock-css', 'html.bc-unlock,html.bc-unlock body{overflow-y:auto!important;overflow-x:hidden!important;position:relative!important;top:auto!important;height:auto!important;touch-action:pan-y pinch-zoom!important;overscroll-behavior:auto!important}', root); root.classList.add('bc-unlock'); [root, document.body].forEach(el => { if (!el) return; [...el.classList].forEach(c => { if (/^(fp_|__fp|noscroll|no_scroll|no-scroll|scroll_lock|scroll-lock|modal_open|modal-open|popup_open|has_popup)/i.test(c)) el.classList.remove(c); }); }); }; if (!window.__bcUnlockBound) { window.__bcUnlockBound = true; const guard = e => { if (!document.documentElement.classList.contains('bc-unlock')) return; const path = e.composedPath ? e.composedPath() : []; if (path.some(n => n && (n.id === 'bc-unchained-host' || n.id === 'bc-cb-qual'))) return; e.stopImmediatePropagation(); }; ['touchmove', 'wheel', 'scroll'].forEach(ev => window.addEventListener(ev, guard, { capture: true, passive: true })); } const MPOP_ATTR = 'data-bc-mpop'; const MPOP_INPUTS = 'input.sendInput,textarea.sendInput,input.js-chat_msg_input,input.ccp_input'; const MPOP_SKIP = '#bChat,#bChatInputContainer,.ccp_wrapper,.chat_panel_wrap,#chat_bar_v2_container,[' + MPOP_ATTR + ']'; const isRootEl = p => p === document.body || p === document.documentElement; const findFixedAncestor = inp => { for (let p = inp.parentElement; p && !isRootEl(p); p = p.parentElement) { if (getComputedStyle(p).position === 'fixed') return p; } return null; }; const findBodyChild = inp => { let p = inp; while (p.parentElement && p.parentElement !== document.body) p = p.parentElement; return p.parentElement === document.body ? p : null; }; const popupTarget = inp => { if (inp.id === 'send-input-container' || inp.closest(MPOP_SKIP)) return null; return findFixedAncestor(inp) || findBodyChild(inp); }; const isModelPopup = (target, vw, vh) => { if (!target || target === document.body) return false; if (target.querySelector('video,#bChat,.chat_panel_wrap,#stream-container')) return false; const r = target.getBoundingClientRect(); return !(r.width > vw * 0.9 && r.height > vh * 0.9); }; const tagModelPopups = () => { if (!BC_RE.test(location.hostname) || !document.body) return; const inputs = document.querySelectorAll(MPOP_INPUTS); if (!inputs.length) return; const vw = window.innerWidth, vh = window.innerHeight; const found = new Set(); inputs.forEach(inp => { const t = popupTarget(inp); if (isModelPopup(t, vw, vh)) found.add(t); }); if (found.size) requestAnimationFrame(() => found.forEach(t => t.setAttribute(MPOP_ATTR, '1'))); }; (() => { let t = 0; const run = () => { clearTimeout(t); t = 0; if (realHidden()) return; try { tagModelPopups(); } catch (e) { dbg(e, 'run'); } try { unlockScroll(); } catch (e) { dbg(e, 'run'); } }; const sched = () => { if (!t) t = setTimeout(run, 150); }; DomBus.subscribe((recs, isOwn) => { if (!isOwn()) sched(); }); onPageEvent('visibilitychange', () => { if (!realHidden()) sched(); }); setInterval(run, 5000); sched(); })(); const COLS_KEY = 'bc_bm_cols_g'; const colsGet = () => (Store.get(COLS_KEY, 1) === 2 ? 2 : 1); const pSvg = (d, s = 20, fill = 'none', stroke = 'currentColor') => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" stroke="${stroke}" fill="${fill}" stroke-width="2">${d}</svg>`; const COLS_ICONS = [ pSvg('<rect x="4" y="5" width="16" height="14" rx="2.5"/>', 18), pSvg('<rect x="3" y="5" width="7.5" height="14" rx="2"/><rect x="13.5" y="5" width="7.5" height="14" rx="2"/>', 18) ]; const icons = { coffee: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M8 3a2.4 2.4 0 0 0-1 2 2.4 2.4 0 0 0 1 2"/><path d="M12 3a2.4 2.4 0 0 0-1 2 2.4 2.4 0 0 0 1 2"/><path d="M3 10h14v5a6 6 0 0 1-6 6H9a6 6 0 0 1-6-6v-5z"/><path d="M16.75 16.73a3 3 0 1 0 .25-5.56"/></g>', 16), color: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 0 0 0 18c1.2 0 2-.9 2-2 0-.6-.3-1-.6-1.4-.3-.4-.4-.8-.4-1.2 0-1.1.9-2 2-2H17a4 4 0 0 0 4-4c0-4.5-4-7.4-9-7.4z"/><circle cx="7.5" cy="11" r="1" fill="currentColor"/><circle cx="10" cy="7" r="1" fill="currentColor"/><circle cx="15" cy="7" r="1" fill="currentColor"/></g>', 14), menu: pSvg('<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>', 24), eye: pSvg('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>', 14), close: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/></g>', 16), home: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M4 10.5V19a1.5 1.5 0 0 0 1.5 1.5H9v-5.5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5.5h3.5A1.5 1.5 0 0 0 20 19v-8.5"/><path d="M2.5 11.5L12 3.5l9.5 8"/></g>', 16), rel: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4.5V9h-4.5"/></g>', 16), min: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M6 9.5l6 6 6-6"/></g>', 16), bgOn: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><polygon points="10 8.5 10 15.5 16 12" fill="currentColor"/></g>'), bgOff: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><polygon points="10 8.5 10 15.5 16 12" fill="currentColor"/><line x1="3" y1="3" x2="21" y2="21"/></g>'), vol: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M15 9.5v5M18.5 7v10M22 10.5v3"/></g>'), mute: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M16 9.5l5 5M21 9.5l-5 5"/></g>'), volLow: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M15 9.5v5"/></g>'), volMed: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M15 9.5v5M18.5 7v10"/></g>'), snap: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V5a2 2 0 0 1 2-2h3M16 3h3a2 2 0 0 1 2 2v3M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3"/><circle cx="12" cy="12" r="3.5"/></g>'), rec: pSvg('<g><rect x="2" y="6" width="20" height="12" rx="6"/><circle cx="12" cy="12" r="3.5" fill="currentColor" stroke="none"/></g>'), qual: pSvg('<rect x="2" y="5" width="20" height="14" rx="3"/><path d="M6.5 9v6M11 9v6M6.5 12H11M14.5 9v6h1.8a2.7 2.7 0 0 0 2.7-2.7v-.6A2.7 2.7 0 0 0 16.3 9z"/>'), pip: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="4.5" width="19" height="15" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></g>'), fs: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></g>'), thtr: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><rect x="5" y="9" width="14" height="6" rx="1" fill="currentColor"/></g>'), search: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="7"/><path d="M16 16l5 5M10.5 7.5v6M7.5 10.5h6"/></g>'), copy: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></g>'), fav: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4.5L5 21V4a1 1 0 0 1 1-1z"/><polygon points="12.00 7.30 13.00 9.82 15.71 9.99 13.62 11.73 14.29 14.36 12.00 12.90 9.71 14.36 10.38 11.73 8.29 9.99 11.00 9.82" fill="currentColor" stroke="currentColor" stroke-width="0.8"/></g>'), favPrev: pSvg('<polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5"/>'), favNext: pSvg('<polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/>'), del: pSvg('<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>', 16), hls: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4.5l2.5-6 4 12 3-8 2 2H22"/></g>'), get ok() { return pSvg('<polyline points="20 6 9 17 4 12"/>', 16, 'none', '#2ecc71'); }, arrowUp: pSvg('<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>', 12), arrowDown: pSvg('<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>', 12), back: pSvg('<polyline points="15 18 9 12 15 6"/>', 18), cam: pSvg('<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>', 12), clock: pSvg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', 12), name: pSvg('<path d="M4 20l5-12 5 12"/><path d="M6 15h6"/>', 12), get dot() { const c = '#2ecc71'; return pSvg('<circle cx="12" cy="12" r="7"/>', 12, c, c); }, list: pSvg('<line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/>', 12), grid: pSvg('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>', 12) }; icons.sleep = pSvg('<circle cx="12" cy="13" r="8"/><polyline points="12 9 12 13 15 15"/><line x1="9" y1="2" x2="15" y2="2"/>'); icons.cal = pSvg('<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>', 15); icons.circle = pSvg('<circle cx="12" cy="12" r="9"/>', 16); icons.check = pSvg('<polyline points="20 6 9 17 4 12"/>', 16); icons.eyeNo = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><path d="M9 9l6 6M15 9l-6 6"/></g>', 16); icons.eyeOk = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><path d="M8.3 12.2l2.7 2.7 4.8-5.4"/></g>', 16); icons.tag = pSvg('<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><circle cx="7" cy="7" r="1"/>', 12); icons.folder = pSvg('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>', 12); icons.chevL = pSvg('<polyline points="15 18 9 12 15 6"/>', 18); icons.chevR = pSvg('<polyline points="9 18 15 12 9 6"/>', 18); icons.boxOn = pSvg('<rect x="3" y="3" width="18" height="18" rx="4"/><polyline points="8 12 11 15 16 9"/>', 16); icons.boxOff = pSvg('<rect x="3" y="3" width="18" height="18" rx="4"/>', 16); icons.undo = pSvg('<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>', 14); icons.eyeOff = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/><line x1="4" y1="20" x2="20" y2="4"/></g>', 14); icons.sun = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></g>', 14); icons.moon = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/><path d="M18 1.5v3M16.5 3h3"/></g>', 14); icons.dl = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><path d="M12 7.5v8M8.5 12.5l3.5 3.5 3.5-3.5"/></g>', 14); icons.ul = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="2.5" width="19" height="19" rx="5"/><path d="M12 16.5v-8M8.5 12l3.5-3.5 3.5 3.5"/></g>', 14); icons.target = pSvg('<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>', 14); icons.globe = pSvg('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>', 15); icons.dice = pSvg('<rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1"/><circle cx="15.5" cy="8.5" r="1"/><circle cx="8.5" cy="15.5" r="1"/><circle cx="15.5" cy="15.5" r="1"/>', 15); icons.gear = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18"/><circle cx="15" cy="6" r="2.2" fill="currentColor"/><circle cx="8" cy="12" r="2.2" fill="currentColor"/><circle cx="17" cy="18" r="2.2" fill="currentColor"/></g>', 15); icons.star = pSvg('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>', 16); icons.history = pSvg('<path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><polyline points="3 3 3 8 8 8"/><polyline points="12 7 12 12 15 14"/>', 15); icons.bookmark = pSvg('<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>', 15); icons.dots = pSvg('<circle cx="5" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="19" cy="12" r="1.4"/>', 18); icons.table = pSvg('<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="4" x2="9" y2="20"/>', 15); const SUPPORT_REF_URL = 'https://chaturbate.com/in/?tour=g4pe&campaign=opzS6&track=default'; const supT = k => (SUPPORT_TXT[AppState.lang] || SUPPORT_TXT.EN)[k]; const openSupportLink = () => { try { window.open(SUPPORT_REF_URL, '_blank', 'noopener,noreferrer'); } catch (e) { dbg(e, 'openSupportLink'); } }; const L = () => i18n[AppState.lang] || i18n.EN; const fmtAgo = ts => { const l = L(), m = Math.max(0, Date.now() - ts) / 60000; if (m < 1) return l.seen_now; const n = m < 60 ? `${Math.floor(m)} ${l.u_m}` : m < 1440 ? `${Math.floor(m / 60)} ${l.u_h}` : `${Math.floor(m / 1440)} ${l.u_d}`; return l.ago.replace('{n}', n); }; const CSS_NOSEL = `*{-webkit-tap-highlight-color:transparent}.panel,.panel *{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}.panel button,.panel [role=button],.panel div,.panel span{outline:0}.panel input,.panel textarea{-webkit-user-select:text;user-select:text}`; const CSS_UI = `:host{--th:${AppState.theme};pointer-events:none;position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;height:0;z-index:2147483647;font-family:system-ui,-apple-system,sans-serif}*{box-sizing:border-box;margin:0;padding:0}.h-btn,.btn,.restore,.ghost,.vico,.z-btn,.bm-close,.bm-del,.bm-act-btn,.resize-handle{display:flex;align-items:center;justify-content:center}.panel{user-select:none;touch-action:none;position:absolute;width:304px;padding:12px;border-radius:14px;pointer-events:auto;background:rgba(15,15,18,.95);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 30px rgba(0,0,0,.7),inset 0 1px 1px rgba(255,255,255,.1);backdrop-filter:blur(24px) saturate(150%);transform-origin:top left;will-change:transform,left,top}.panel.mini{width:48px;height:48px;padding:0;background:transparent;border:none;box-shadow:none;backdrop-filter:none}.panel.hidden{display:none!important}.col{display:flex;flex-direction:column;gap:8px}.grid-4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));grid-auto-rows:56px;gap:6px} .grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-top:6px}.sep{height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.15),transparent);margin:2px 0}.hdr{display:flex;justify-content:space-between;align-items:center;background:rgba(0,0,0,.35);border-radius:8px;padding:6px 8px;margin-bottom:6px}.drag{flex:0 0 0;width:0;align-self:stretch;min-height:0;min-width:0;padding:0!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:rgba(255,255,255,.6);font-size:9.5px;font-weight:600;letter-spacing:1px;text-transform:uppercase;cursor:grab;display:flex;align-items:center;justify-content:flex-start;padding-left:4px;user-select:none;touch-action:none!important;margin-right:auto}.drag:active{cursor:grabbing}.stats{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;background:rgba(0,0,0,.5);color:#fff;font-size:9px;min-width:30px;height:26px;padding:2px 4px;border-radius:4px;font-weight:bold;margin-right:2px;box-sizing:border-box;line-height:1;flex-shrink:0}.hdr-btns{display:flex;gap:5px;align-items:center;flex:1 1 auto;margin-left:0;justify-content:space-between}.h-btn{width:24px;height:24px;background:rgba(255,255,255,.05);border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:12px;transition:.2s}.h-btn:hover{background:rgba(255,255,255,.2);transform:translateY(-1px)}.h-btn.close:hover{background:#e74c3c;transform:none}.h-btn.lang{width:28px;font-weight:bold;background:rgba(255,255,255,.1);font-size:10px}.btn{color:#fff;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.05);border-radius:8px;padding:8px 2px 6px;font-size:18px;cursor:pointer;flex-direction:column;width:100%;min-width:0;height:56px;min-height:56px;max-height:56px;overflow:hidden;transition:all .2s ease;outline:none}.btn:hover{background:rgba(255,255,255,.15);transform:translateY(-2px);border-color:rgba(255,255,255,.2)}.btn:active{transform:scale(.92)}.btn.on{background:var(--th);box-shadow:0 4px 10px rgba(0,0,0,.3);border-color:transparent}.btn sub{font-size:8px;font-weight:600;margin-top:4px;letter-spacing:.5px;text-transform:uppercase;opacity:.8;pointer-events:none;text-align:center;white-space:nowrap;word-break:normal;line-height:1}.restore{width:48px;height:48px;border-radius:22px;font-size:22px;background:var(--th);border:2px solid rgba(255,255,255,.3);display:none;cursor:pointer;pointer-events:auto;box-shadow:0 4px 15px rgba(0,0,0,.6);transition:.2s;touch-action:none!important}.restore:hover{transform:scale(1.05)}.ghost{position:absolute;bottom:80px;right:16px;width:44px;height:44px;border-radius:20px;font-size:20px;background:var(--th);border:2px solid rgba(255,255,255,.35);display:none;cursor:pointer;pointer-events:auto;box-shadow:0 4px 15px rgba(0,0,0,.6);transition:transform .2s,opacity .2s;touch-action:manipulation;z-index:2147483647}.ghost:hover,.ghost:active{transform:scale(1.1)}.v-cap{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.4);border-radius:10px;padding:8px 12px;border:1px solid rgba(255,255,255,.05)}.vico{color:#fff;font-size:16px;cursor:pointer;flex-shrink:0;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.vslider{-webkit-appearance:none;appearance:none;flex:1;height:3px;border-radius:1px;outline:none;cursor:pointer;background:linear-gradient(to right,var(--th) 0%,var(--th) var(--p,100%),rgba(255,255,255,.1) var(--p,100%),rgba(255,255,255,.1) 100%)}.vslider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 0 5px rgba(0,0,0,.5);border:2px solid var(--th);cursor:pointer;transition:.2s}.vslider::-webkit-slider-thumb:hover{transform:scale(1.3)}.vval{font-size:10px;font-weight:600;color:#ccc;min-width:32px;text-align:right}.row-4{display:flex;align-items:center;justify-content:space-between;gap:8px}.scale-ctrl{display:flex;align-items:center;gap:4px;background:rgba(0,0,0,.3);border-radius:4px;padding:2px}.z-btn{width:20px;height:20px;background:rgba(255,255,255,.05);border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:14px;transition:.2s;outline:none}.z-btn:hover{background:rgba(255,255,255,.2)}.s-val{font-size:9px;color:#ccc;min-width:24px;text-align:center;font-weight:bold}.hotkeys{font-size:9px;color:rgba(255,255,255,.4);text-align:center;font-weight:500;letter-spacing:.5px;padding-top:2px}.bm-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:.3s;backdrop-filter:blur(5px);z-index:2147483647}.bm-overlay.open{opacity:1;pointer-events:auto}.bm-modal{width:370px;max-width:96vw;height:90vh;max-height:90vh;min-height:450px;background:rgb(20,20,25);border:1px solid rgba(255,255,255,.1);border-radius:14px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.8)}.bm-hdr{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:rgba(0,0,0,.4);border-bottom:1px solid rgba(255,255,255,.05);flex-shrink:0}.bm-title{color:#fff;font-weight:bold;font-size:13px;white-space:nowrap;flex-shrink:0}.bm-close{background:none;border:none;color:#aaa;font-size:26px;cursor:pointer;padding:2px 8px;line-height:1;min-width:32px;min-height:32px}.bm-close:hover{color:#fff}.bm-list{flex:1;min-height:0;overflow-y:scroll;scrollbar-gutter:stable;padding:12px;display:flex;flex-direction:column;gap:10px}.bm-item{display:flex;flex-direction:column;gap:8px;padding:10px;background:rgba(255,255,255,.05);border-radius:10px;transition:.2s}.bm-item:hover{background:rgba(255,255,255,.08)}.bm-item-top{display:flex;align-items:center;justify-content:space-between;gap:10px}.bm-link{display:flex;align-items:center;gap:10px;text-decoration:none;color:#fff;flex:1;overflow:hidden}.bm-link:hover .bm-name{color:var(--th)}.bm-avatar{width:42px;height:42px;border-radius:50%;object-fit:cover;aspect-ratio:1/1;background:#333;border:1px solid rgba(255,255,255,.1);flex-shrink:0}.bm-name{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:.2s}.bm-del{width:30px;height:30px;border-radius:4px;background:rgba(231,76,60,.2);color:#e74c3c;border:none;cursor:pointer;transition:.2s;flex-shrink:0}.bm-del:hover{background:#e74c3c;color:#fff}.bm-del svg,.bm-modal .bm-del svg{width:14px;height:14px;margin:0!important;display:block;vertical-align:baseline}.bm-act-btn{height:28px;padding:0 8px;border-radius:4px;background:rgba(255,255,255,.06);color:#ccc;border:1px solid rgba(255,255,255,.08);gap:4px;cursor:pointer;font-size:11px;font-weight:600;transition:.2s}.bm-act-btn:hover{background:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.2)}.bm-act-btn svg{width:13px;height:13px;flex-shrink:0}.bm-note{width:100%;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);color:#fff;border-radius:6px;padding:8px 10px;font-size:13px;font-family:inherit;outline:none;transition:.2s}.bm-note:focus{border-color:var(--th);background:rgba(0,0,0,.6);box-shadow:0 0 5px rgba(255,255,255,.1)}.bm-foot{padding:12px;display:grid;grid-template-columns:1fr 1fr;gap:8px;background:rgba(0,0,0,.4);border-top:1px solid rgba(255,255,255,.05);flex-shrink:0}.bm-add{grid-column:span 2;padding:12px;border-radius:6px;border:none;background:var(--th);color:#fff;font-weight:bold;cursor:pointer;font-size:14px;box-shadow:0 2px 10px rgba(0,0,0,.3);transition:.2s}.bm-add:hover{filter:brightness(1.2)}.bm-add:disabled{background:#555;cursor:not-allowed;opacity:.5}.bm-btn{padding:10px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#ddd;font-size:13px;font-weight:600;cursor:pointer;transition:.2s}.bm-btn:hover{background:rgba(255,255,255,.15);color:#fff}.bm-empty{text-align:center;padding:30px 10px;color:#777;font-size:14px;grid-column:1 / -1}.bm-gallery-mode{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));align-content:start;gap:10px;padding:10px}.bm-item{min-width:0}.bm-gallery-mode .bm-item{min-width:0;padding:10px;cursor:pointer;display:flex;flex-direction:column;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:10px;position:relative;gap:6px}.bm-gallery-mode .bm-item:hover{transform:translateY(-2px);z-index:10;border-color:var(--th);background:rgba(255,255,255,.08);box-shadow:0 4px 12px rgba(0,0,0,.5)}.bm-gallery-mode .bm-topic,.bm-gallery-mode .bm-note{display:none!important}.bm-gallery-mode .bm-item-top{display:contents}.bm-gallery-mode .bm-link{display:contents}.bm-gallery-mode .bm-link>div{order:1;display:flex;align-items:center;width:calc(100% - 32px);max-width:calc(100% - 32px);pointer-events:none}.bm-gallery-mode .bm-del{order:2;position:absolute;top:6px;right:6px;width:24px;height:24px;z-index:2;margin:0;border-radius:4px}.bm-gallery-mode .bm-avatar{order:3;width:100%!important;height:auto!important;aspect-ratio:16/9!important;border-radius:6px;object-fit:cover;object-position:center;border:1px solid rgba(0,0,0,.3);pointer-events:none;margin:0;display:block!important;filter:brightness(.75)}.bm-gallery-mode .bm-item:has(img[data-frame]) .bm-avatar{display:none!important}.bm-gallery-mode img[data-frame]{order:3;width:100%!important;height:auto!important;aspect-ratio:16/9!important;border-radius:6px;object-fit:cover;border:1px solid rgba(0,0,0,.3)!important;margin:0!important;pointer-events:none;display:block!important}.bm-gallery-mode .bm-actions-row{order:4;display:grid!important;width:100%;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:auto;padding-top:2px}.bm-gallery-mode .bm-act-btn{height:28px;padding:0;background:rgba(0,0,0,.4);border-radius:4px;border:1px solid rgba(255,255,255,.05)}.bm-gallery-mode .bm-act-btn span{display:none}.bm-gallery-mode .bm-act-btn svg{margin:0;width:14px;height:14px}.bm-gallery-mode .bm-name{font-size:12px;color:#eee;font-weight:bold;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (min-width:768px){.bm-modal{width:780px!important;max-width:92vw!important;height:85vh!important;max-height:85vh!important}.bm-list{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;align-content:start!important;gap:12px!important}.bm-gallery-mode{grid-template-columns:repeat(3,minmax(0,1fr))!important;gap:12px!important}}@media (min-width:1200px){.bm-modal{width:920px!important}.bm-gallery-mode{grid-template-columns:repeat(4,minmax(0,1fr))!important}}.bm-list::-webkit-scrollbar{width:6px}.bm-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:2px}@keyframes pulseRec{0%,100%{background:rgba(255,255,255,.06);box-shadow:none}50%{background:#c0392b;box-shadow:0 2px 8px rgba(0,0,0,.5)}}.rec-active{animation:pulseRec 1.5s infinite!important;border-color:transparent!important}.resize-handle{position:absolute;right:2px;bottom:2px;width:16px;height:16px;cursor:nwse-resize;touch-action:none!important;opacity:.4;transition:opacity .2s;z-index:10}.resize-handle:hover{opacity:1}.resize-handle::after{content:none}.panel.mini .resize-handle{display:none!important}.rs-edge{position:absolute;z-index:9;touch-action:none!important}.rs-n{top:-5px;left:14px;right:14px;height:12px;cursor:ns-resize}.rs-s{bottom:-5px;left:14px;right:14px;height:12px;cursor:ns-resize}.rs-w{left:-5px;top:14px;bottom:14px;width:12px;cursor:ew-resize}.rs-e{right:-5px;top:14px;bottom:14px;width:12px;cursor:ew-resize}.rs-nw{top:-5px;left:-5px;width:18px;height:18px;cursor:nwse-resize}.rs-ne{top:-5px;right:-5px;width:18px;height:18px;cursor:nesw-resize}.rs-sw{bottom:-5px;left:-5px;width:18px;height:18px;cursor:nesw-resize}.panel.mini .rs-edge{display:none!important}@media(pointer:coarse){.rs-n,.rs-s{height:22px}.rs-n{top:-8px}.rs-s{bottom:-8px}.rs-w,.rs-e{width:22px}.rs-w{left:-8px}.rs-e{right:-8px}.rs-nw,.rs-ne,.rs-sw{width:28px;height:28px}.rs-nw{top:-8px;left:-8px}.rs-ne{top:-8px;right:-8px}.rs-sw{bottom:-8px;left:-8px}}.bm-prev:not([data-frame]){display:none!important}.bm-prev.bm-bad{visibility:hidden}.bm-stat{width:8px;height:8px;border-radius:50%;background:#888;margin-right:8px;flex-shrink:0;box-shadow:0 0 4px rgba(0,0,0,.5);transition:.3s}.bm-nwrap{display:flex;align-items:center;min-width:0;flex:1}.bm-acts{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:4px}.bm-topic{font-size:11px;color:#ddd;background:rgba(0,0,0,.35);padding:6px 10px;border-radius:4px;margin:6px 0;word-break:break-word;white-space:pre-wrap;display:none;border:1px solid rgba(255,255,255,.08)}.bm-prev{width:100%;max-height:180px;object-fit:cover;border-radius:3px;margin-top:8px;display:none;border:1px solid rgba(255,255,255,.1);cursor:pointer}.bm-tbtn{display:flex;align-items:center;font-size:11px;font-weight:bold;padding:4px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.05);color:#fff;cursor:pointer;transition:.2s;margin-right:4px;flex-shrink:0;box-sizing:border-box}.bm-tbtn.on{background:var(--th)}.bm-hbtn{font-size:11px;cursor:pointer;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.15);color:#fff;border-radius:4px;display:flex;align-items:center;padding:3px 7px}.bm-sort-wrap{display:flex;align-items:center;gap:4px;margin-left:auto;margin-right:6px}.bm-tags-wrap{display:none;flex-wrap:wrap;gap:6px;padding:4px 12px 10px;background:rgba(0,0,0,.4);flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.05)}.bm-modal button svg{vertical-align:middle}.bm-title svg{margin-right:4px;vertical-align:-2px}.bm-twrap{display:flex;align-items:center;gap:2px;flex:1}.hd-sec{grid-column:1 / -1;color:#888;font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;padding:6px 4px 0}.hd-hint{grid-column:1 / -1;color:#777;font-size:11px;padding:0 4px}.bm-overlay.peek{opacity:0!important;background:transparent!important;backdrop-filter:none!important;transition:opacity .12s!important}.pick-box{position:fixed;display:none;pointer-events:none;border:2px solid #e74c3c;background:rgba(231,76,60,.12);box-sizing:border-box;z-index:2147483646}.pick-bar{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:none;flex-direction:column;gap:8px;width:min(92vw,360px);padding:10px;background:rgba(15,15,18,.95);border:1px solid rgba(255,255,255,.15);border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.7);pointer-events:auto;z-index:2147483647}.pick-bar.open{display:flex}.pick-info{color:#ddd;font-size:11px;font-family:monospace;word-break:break-all;max-height:56px;overflow:hidden}.pick-btns{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}.bm-grp{width:100%;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);color:#ddd;border-radius:6px;padding:7px 8px;font-size:12px;font-family:inherit;outline:none}.bm-grp option{background:#1b1b1f;color:#fff}.bm-gallery-mode .bm-grp{display:none!important}.bm-seen{flex-shrink:0;margin-left:8px;font-size:10px;color:#8a8a90;white-space:nowrap}.bm-modal.rs-modal{width:380px!important;max-width:94vw!important;height:auto!important;min-height:0!important;max-height:90vh!important}.rs-modal .bm-list{display:flex!important;flex-direction:column!important;flex:0 1 auto;overflow-y:auto;scrollbar-gutter:auto;gap:8px}.rs-qs{display:flex;flex-direction:column;gap:6px}.rs-qs .bm-btn{text-align:left}.rs-chips{display:flex;flex-wrap:wrap;gap:6px}.rs-chips .bm-tbtn{margin-right:0;padding:7px 12px;font-size:12px}.rs-info{font-size:12px;color:#ddd;min-height:16px;padding:2px 4px}.rs-note{font-size:11px;color:#8a8a90;padding:0 4px;line-height:1.35}.bm-btn.sel{background:var(--th);color:#fff;border-color:transparent} .bm-overlay.fav-ov{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.8);transition:opacity .18s ease} .fav-modal{width:min(980px,96vw)!important;max-width:96vw!important;height:88vh!important;max-height:88vh!important;min-height:0!important;border-radius:18px;background:linear-gradient(180deg,rgba(30,30,37,.97),rgba(15,15,19,.98));border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 70px rgba(0,0,0,.7)} .fav-modal .bm-hdr{position:relative;flex-wrap:wrap;justify-content:flex-start;gap:8px;padding:14px 16px 12px;padding-inline-end:58px;background:rgba(255,255,255,.03);border-bottom:1px solid rgba(255,255,255,.06)} .fav-modal .bm-title{display:flex;align-items:center;font-size:17px;font-weight:800;letter-spacing:.2px} .fav-modal .bm-close{position:absolute;top:10px;inset-inline-end:12px;width:36px;height:36px;min-width:0;min-height:0;padding:0;border-radius:50%;background:rgba(255,255,255,.07);font-size:22px;color:#ccc} .fav-modal .bm-close:hover{background:rgba(231,76,60,.85);color:#fff} .fav-modal .bm-hdr>.bm-hbtn{margin-left:0!important} .fav-modal .bm-hdr .bm-title{flex:1 1 auto;min-width:0}.fav-modal .bm-tools{order:3;flex:1 0 100%;display:flex;align-items:center;gap:8px;justify-content:center;justify-content:safe center;overflow-x:auto;scrollbar-width:none}.fav-modal .bm-tools::-webkit-scrollbar{display:none}.fav-modal .bm-tools>*{flex:0 0 auto;margin:0!important}.fav-modal .bm-tools .bm-hbtn{height:36px;min-width:40px;justify-content:center}.fav-modal .bm-tools .bm-hbtn.on{background:var(--th);border-color:transparent}.fav-modal .bm-tools .bm-sort-wrap .bm-hbtn.on{background:transparent} .fav-modal .bm-hbtn{display:inline-flex;align-items:center;justify-content:center;line-height:1}.fav-modal .bm-hbtn svg{display:block;flex:none;margin:0!important}.fav-modal .bm-hdr{padding-inline-end:16px!important}.fav-modal .bm-grp-btn{display:flex!important;align-items:center;justify-content:space-between!important;gap:8px;width:100%;text-align:start!important}.fav-modal .bm-grp-btn>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fav-modal .bm-grp-arr{flex:none}.fav-modal .bm-tags-wrap{flex-direction:column!important;flex-wrap:nowrap!important;align-items:stretch;gap:10px!important;max-height:none!important;overflow:visible!important;padding:12px 16px!important}.fav-modal .bm-seg{display:flex;border:1px solid rgba(255,255,255,.12);border-radius:8px;overflow:hidden;background:rgba(255,255,255,.05)}.fav-modal .bm-seg .bm-tbtn{flex:1 1 0;min-width:0;height:36px;margin:0;padding:0 8px;border:0;border-radius:0;background:transparent;box-shadow:none;display:flex;align-items:center;justify-content:center;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fav-modal .bm-seg .bm-tbtn+.bm-tbtn{border-left:1px solid rgba(255,255,255,.12)}.fav-modal .bm-seg .bm-tbtn.on{background:var(--th)}.fav-modal .bm-seg svg{display:block;flex:none;margin:0;width:16px;height:16px}.fav-modal .bm-chips{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;margin:0 -16px;padding:0 16px}.fav-modal .bm-chips::-webkit-scrollbar{display:none}.fav-modal .bm-chips .bm-tbtn{flex:none;padding:6px 14px}.fav-modal .bm-hdr .bm-title{padding-inline-end:48px;min-height:38px}.fav-modal .bm-hdr{row-gap:10px}.fav-modal .bm-tools{overflow:visible;gap:10px}.fav-modal .bm-tools>.bm-sort-wrap{flex:1 1 auto!important;display:flex}.fav-modal .bm-tools .bm-sort-wrap .bm-hbtn:first-child{flex:1 1 auto}.fav-modal .bm-tools .bm-sort-wrap .bm-hbtn:last-child{width:46px}.fav-modal .bm-setp{padding:2px 14px;gap:0;border:1px solid rgba(255,255,255,.08)}.fav-modal .bm-setrow+.bm-setrow{border-top:1px solid rgba(255,255,255,.06)}.fav-modal .bm-tools .bm-gear{width:48px;flex:none} .fav-modal .bm-gear{width:48px;padding:0}.fav-modal .bm-gear svg{width:18px;height:18px}.fav-modal .bm-setp{display:none;flex:1 0 100%;order:4;flex-direction:column;gap:2px;padding:4px 10px;border-radius:10px;background:rgba(0,0,0,.28)}.fav-modal .bm-setp.open{display:flex}.fav-modal .bm-setrow{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;font-size:14px;color:#ddd}.fav-modal .bm-sw{position:relative;flex:none;width:46px;height:26px;padding:0;border:0;border-radius:13px;background:rgba(255,255,255,.18);cursor:pointer;transition:.2s}.fav-modal .bm-sw::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:.2s}.fav-modal .bm-sw.on{background:var(--th)}.fav-modal .bm-sw.on::after{left:23px}.fav-modal .bm-spd{height:30px;min-width:52px}.fav-modal.bm-no-topic .bm-topic{display:none!important}.fav-modal.bm-no-grp .bm-grp-btn{display:none!important} .fav-modal .bm-sort-wrap{gap:0;margin:0;margin-inline-start:auto;border:1px solid rgba(255,255,255,.12);border-radius:8px;overflow:hidden;background:rgba(255,255,255,.05)} .fav-modal .bm-hbtn{height:30px;padding:0 10px;gap:4px;border-radius:8px;font-weight:600;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s} .fav-modal .bm-hbtn:hover{background:rgba(255,255,255,.14)} .fav-modal .bm-sort-wrap .bm-hbtn{border:0;border-radius:0;background:transparent} .fav-modal .bm-sort-wrap .bm-hbtn:hover{background:rgba(255,255,255,.1)} .fav-modal .bm-sort-wrap .bm-hbtn+.bm-hbtn{border-left:1px solid rgba(255,255,255,.12)} .fav-modal .bm-sort-wrap .bm-hbtn:last-child{padding:0 9px} .fav-modal .bm-sort-wrap .bm-hbtn:last-child svg{margin:0} .fav-modal .bm-tags-wrap{gap:6px;padding:10px 16px;max-height:92px;overflow-y:auto;background:transparent;border-bottom:1px solid rgba(255,255,255,.06)} .fav-modal .bm-tbtn{margin:0;padding:6px 12px;border-radius:999px;font-size:12px;font-weight:600;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s} .fav-modal .bm-tbtn:hover{background:rgba(255,255,255,.14)} .fav-modal .bm-tbtn.on{background:var(--th);border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)} .fav-modal .bm-list{grid-template-columns:repeat(auto-fill,minmax(min(300px,100%),1fr))!important;gap:12px!important;padding:14px!important;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .fav-modal .bm-list.bm-gallery-mode{grid-template-columns:repeat(auto-fill,minmax(min(150px,100%),1fr))!important;gap:10px!important} @media(min-width:760px){.fav-modal .bm-list.bm-gallery-mode{grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),1fr))!important}} .fav-modal .bm-item{position:relative;padding:12px 12px 12px 16px;gap:10px;border-radius:14px;background:linear-gradient(180deg,rgba(255,255,255,.065),rgba(255,255,255,.03));border:1px solid rgba(255,255,255,.08);transition:transform .2s,border-color .2s,background .2s,box-shadow .2s} @media (hover:hover) and (pointer:fine){.fav-modal .bm-item:hover{background:linear-gradient(180deg,rgba(255,255,255,.09),rgba(255,255,255,.05));border-color:rgba(255,255,255,.18)}} .fav-modal .bm-item,.fav-modal button{-webkit-tap-highlight-color:transparent} .fav-modal .bm-item[data-st="0"]{--st:rgba(255,255,255,.14)} .fav-modal .bm-item[data-st="1"]{--st:#2ecc71} .fav-modal .bm-item[data-st="2"]{--st:#f39c12} .fav-modal .bm-item[data-st="3"]{--st:#9b59b6} .fav-modal .bm-item::before{content:"";position:absolute;left:0;top:14px;bottom:14px;width:3px;border-radius:0 2px 2px 0;background:var(--st,rgba(255,255,255,.14));transition:background .3s} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-avatar{box-sizing:border-box;width:48px;height:48px;padding:0;border:2px solid var(--st,rgba(255,255,255,.14));background-clip:padding-box;box-shadow:none} .fav-modal .bm-avatar{transition:filter .3s,border-color .3s,box-shadow .3s} .fav-modal .bm-item[data-st="0"] .bm-avatar{filter:grayscale(.8) brightness(.8)} .fav-modal .bm-name{font-size:15px;font-weight:700;letter-spacing:.1px} .fav-modal .bm-seen{font-size:11px;color:#9a9aa2} .fav-modal .bm-list .bm-stat{display:none} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-act-btn{background:rgba(0,0,0,.4);border-color:rgba(255,255,255,.05)} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-act-btn:hover{background:rgba(0,0,0,.25);border-color:rgba(255,255,255,.15)} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-pin:not(.on),.fav-modal .bm-list:not(.bm-gallery-mode) .bm-del{background:rgba(0,0,0,.55);border-color:rgba(255,255,255,.1)} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-del:hover{background:#e74c3c} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-nwrap .bm-view:not(:empty){margin-inline-start:8px;padding:3px 6px;gap:3px;border-radius:7px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#cfcfd6;font-size:11px} .fav-modal .bm-stat{width:9px;height:9px} .fav-modal .bm-del{width:32px;height:32px;border-radius:8px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);color:#d9707e} .fav-modal .bm-del:hover{background:#e74c3c;border-color:transparent;color:#fff} .fav-modal .bm-acts{gap:6px} .fav-modal .bm-act-btn{height:32px;border-radius:8px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);color:#d4d4da;font-size:12px} .fav-modal .bm-act-btn:hover{background:var(--th);border-color:transparent;color:#fff} .fav-modal .bm-note,.fav-modal .bm-grp{background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 10px;font-size:13px;color:#eee} .fav-modal .bm-note:focus,.fav-modal .bm-grp:focus{border-color:var(--th);background:rgba(0,0,0,.5);box-shadow:0 0 0 3px rgba(255,255,255,.06)} .fav-modal .bm-topic{margin:0;padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.06);border-left:3px solid var(--th);background:rgba(0,0,0,.28);font-size:12px;line-height:1.4;color:#d0d0d6} .fav-modal .bm-prev{margin-top:0;border-radius:3px;width:100%;aspect-ratio:16/9;max-height:none;object-fit:cover;background:rgba(255,255,255,.05)} .fav-modal .bm-gallery-mode .bm-item{padding:8px;border-radius:14px;gap:8px} .fav-modal .bm-gallery-mode .bm-item::before{display:none} .fav-modal .bm-gallery-mode .bm-item[data-st="1"]{border-color:rgba(46,204,113,.55)} .fav-modal .bm-gallery-mode .bm-item[data-st="2"]{border-color:rgba(243,156,18,.55)} .fav-modal .bm-gallery-mode .bm-item[data-st="3"]{border-color:rgba(155,89,182,.6)} .fav-modal .bm-gallery-mode .bm-avatar{border-radius:10px} .fav-modal .bm-gallery-mode .bm-name{font-size:12.5px} .fav-modal .bm-gallery-mode .bm-del{width:26px;height:26px;border-radius:6px;background:rgba(0,0,0,.55);border-color:rgba(255,255,255,.1)} .fav-modal .bm-gallery-mode .bm-del:hover{background:#e74c3c} .fav-modal .bm-gallery-mode .bm-act-btn{height:30px;padding:0;background:rgba(0,0,0,.4);border-radius:6px} .fav-modal .bm-gallery-mode .bm-act-btn:hover{background:var(--th)} .fav-modal .bm-foot{padding:12px 16px;background:rgba(255,255,255,.03);border-top:1px solid rgba(255,255,255,.06)} .fav-modal .bm-add{padding:13px;border-radius:10px;font-size:14px;box-shadow:0 6px 18px rgba(0,0,0,.35)} @supports(background:color-mix(in srgb,red,blue)){.fav-modal .bm-add{background:linear-gradient(135deg,var(--th),color-mix(in srgb,var(--th) 65%,#000))}} .fav-modal .bm-add:disabled{background:rgba(255,255,255,.08);box-shadow:none;color:#8a8a90;opacity:1} .fav-modal .bm-empty{padding:70px 12px;color:#8a8a90;font-size:15px} .fav-modal .bm-empty::before{content:"★";display:block;font-size:44px;line-height:1;margin-bottom:10px;opacity:.35} .fav-ov,.fav-modal,.fav-modal .bm-list,.fav-modal .hs-pane,.fav-modal .cal-pane,.fav-modal .bm-tags-wrap{overscroll-behavior:contain} @media(max-width:520px){.fav-modal{width:100vw!important;max-width:100vw!important;height:100%!important;max-height:100%!important;border-radius:0;border:0}.fav-modal .bm-foot{padding-bottom:calc(12px + env(safe-area-inset-bottom,0px))}} .panel .hdr{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:6px 8px;margin-bottom:8px} .panel .drag{color:#9a9aa2;font-weight:700} .panel .stats{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:6px}.panel .stats circle{stroke:var(--th)} .panel .h-btn{width:26px;height:26px;border-radius:50%;background:rgba(255,255,255,.07)} .panel .h-btn.lang{width:30px;border-radius:8px;background:rgba(255,255,255,.1)}.hdr-btns .h-btn svg{width:19px!important;height:19px!important}.hdr-btns .h-btn.lang{border:1px solid var(--th)!important}.hdr-btns .stats{flex-direction:row!important;width:62px!important;min-width:62px!important;max-width:62px!important;flex:0 0 62px!important;justify-content:center!important;height:26px!important;gap:4px!important;padding:0 6px!important;font-size:12px!important;overflow:hidden!important}.hdr-btns .stats svg{width:15px!important;height:15px!important}.hdr-btns .stats span{font-size:12px!important;line-height:1!important}.row-2 .z-btn svg,.row-4 .z-btn svg{width:17px!important;height:17px!important} .panel .h-btn.close:hover{background:rgba(231,76,60,.9);transform:none} .favnav{display:none;grid-template-columns:repeat(2,1fr);gap:6px;margin-top:6px}.favnav.on{display:grid}.favnav .btn{height:32px!important;min-height:0!important;padding:0!important}.favnav .btn sub{display:none!important}.favnav .btn svg{width:16px;height:16px} .panel .btn.on{background:var(--th);border-color:transparent;color:#fff;box-shadow:0 4px 12px rgba(0,0,0,.35)} @supports(background:color-mix(in srgb,red,blue)){.panel .btn.on{background:linear-gradient(135deg,var(--th),color-mix(in srgb,var(--th) 65%,#000))}} .panel .btn.fav-on{position:relative;color:#ffc83d;border-color:rgba(255,200,61,.4)} .panel .z-btn{width:24px;height:24px;border-radius:6px;background:rgba(255,255,255,.1)} .panel:not(.mini)::before{content:'';position:absolute;top:-1px;left:16px;right:16px;height:2px;border-radius:0 0 1px 1px;background:linear-gradient(90deg,transparent,var(--th),transparent);pointer-events:none} .panel .sep{background:linear-gradient(90deg,transparent,var(--th),transparent);opacity:.6} .bm-overlay:not(.fav-ov){backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.8);transition:opacity .18s ease} .bm-modal:not(.fav-modal){border-radius:18px;background:linear-gradient(180deg,rgb(30,30,37),rgb(15,15,19))!important;border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 70px rgba(0,0,0,.7)} .bm-modal:not(.fav-modal) .bm-hdr{position:relative;gap:8px;padding:14px 16px 12px;padding-inline-end:58px;background:rgba(255,255,255,.03);border-bottom:1px solid rgba(255,255,255,.06)} .bm-modal:not(.fav-modal) .bm-title{display:flex;align-items:center;gap:6px;font-size:17px;font-weight:800;letter-spacing:.2px} .bm-modal:not(.fav-modal) .bm-close{position:absolute;top:50%;transform:translateY(-50%);inset-inline-end:12px;width:36px;height:36px;min-width:0;min-height:0;padding:0;border-radius:50%;background:rgba(255,255,255,.07);font-size:22px;color:#ccc;display:flex;align-items:center;justify-content:center;transition:.2s} .bm-modal:not(.fav-modal) .bm-close:hover{background:rgba(231,76,60,.85);color:#fff} .bm-modal:not(.fav-modal) .bm-hbtn{border-radius:8px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s} .bm-modal:not(.fav-modal) .bm-list{padding:14px;gap:10px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .bm-modal:not(.fav-modal) .bm-btn{border-radius:10px;background:rgba(255,255,255,.065);border:1px solid rgba(255,255,255,.1);color:#e2e2e6} .bm-modal:not(.fav-modal) .bm-btn:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.18)} .bm-modal:not(.fav-modal) .bm-btn:active{background:var(--th);border-color:transparent;color:#fff} .bm-modal:not(.fav-modal) .bm-btn.sel,.bm-modal:not(.fav-modal) .bm-btn.sel:hover{background:var(--th);border-color:transparent;color:#fff} .bm-modal:not(.fav-modal) .bm-tbtn{border-radius:999px;font-weight:600;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s} .bm-modal:not(.fav-modal) .bm-tbtn.on{background:var(--th);border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)} .bm-modal:not(.fav-modal) .bm-note{background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.08);border-radius:8px} .bm-modal:not(.fav-modal) .bm-note:focus{border-color:var(--th);background:rgba(0,0,0,.5);box-shadow:0 0 0 3px rgba(255,255,255,.06)} .bm-modal:not(.fav-modal) .hd-sec{color:#9a9aa2} .bm-modal:not(.fav-modal) .bm-foot{padding:12px 16px;background:rgba(255,255,255,.03);border-top:1px solid rgba(255,255,255,.06)} @media(max-width:520px){.bm-modal:not(.fav-modal){border-radius:16px}} .bm-modal .bm-close{display:flex;align-items:center;justify-content:center;padding:0!important;line-height:0;text-align:center} .bm-modal .bm-close svg{display:block;flex:none;margin:0!important;width:16px;height:16px} .bm-modal .bm-close.bm-back{inset-inline-end:56px!important} .bm-modal:not(.fav-modal).has-back .bm-hdr{padding-inline-end:104px!important} .fav-modal.has-back .bm-hdr .bm-title{padding-inline-end:96px!important} :host([data-rtl]) .bm-back svg{transform:scaleX(-1)} .bm-overlay.bm-nofade{transition:none!important} .fav-modal .bm-search-wrap{display:none;padding:10px 16px 0;flex-shrink:0} .fav-modal .bm-search-box{position:relative} .fav-modal .bm-search-ico{position:absolute;inset-inline-start:12px;top:50%;transform:translateY(-50%);display:flex;color:#8a8a90;pointer-events:none} .fav-modal .bm-search-ico svg{width:16px;height:16px} .fav-modal .bm-search-in{width:100%;height:38px;padding:0 40px;border-radius:10px;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.1);color:#f0f0f0;-webkit-text-fill-color:#f0f0f0;font-size:14px;font-family:inherit;outline:none;transition:.2s} .fav-modal .bm-search-in:focus{border-color:var(--th);background:rgba(0,0,0,.5);box-shadow:0 0 0 3px rgba(255,255,255,.06)} .fav-modal .bm-search-in::placeholder{color:#8a8a90} .fav-modal .bm-hdr{padding:8px 12px 8px!important;padding-inline-end:50px!important;gap:6px!important;align-items:center} .fav-modal .bm-title{font-size:16px!important;flex:0 0 auto!important} .fav-modal .bm-close{top:9px!important;inset-inline-end:8px!important;width:32px!important;height:32px!important;font-size:20px!important} .fav-modal .bm-title{flex:1 1 auto!important} .fav-modal .bm-tools{order:2!important;flex:0 0 auto!important;gap:6px!important;justify-content:flex-end!important} .fav-modal .bm-search-wrap{align-items:center;gap:8px} .fav-modal .bm-search-box{flex:1 1 0;min-width:0} .fav-modal .bm-search-wrap>.bm-sort-wrap{flex:0 0 auto!important;margin:0!important;height:34px} .fav-modal .bm-search-wrap>.bm-sort-wrap .bm-hbtn{height:34px!important;padding:0 10px!important} .fav-modal .bm-search-wrap>.bm-sort-wrap .bm-hbtn:last-child{width:38px!important;padding:0!important} .fav-modal .bm-tools .bm-hbtn{height:32px!important;min-width:36px!important} .fav-modal .bm-tools .bm-gear{width:38px!important} .fav-modal .bm-tools>.bm-sort-wrap{min-width:0!important} .fav-modal .bm-setp{order:4} .fav-modal .bm-search-wrap{padding:6px 12px 0!important} .fav-modal .bm-search-in{height:34px!important;font-size:13px!important} .fav-modal .bm-tags-wrap{flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;gap:8px!important;padding:6px 12px!important;max-height:none!important;overflow:hidden!important} .fav-modal .bm-seg{flex:1 1 100%;min-width:0;max-width:100%} .fav-modal .bm-seg .bm-tbtn{height:30px!important;padding:0 12px!important;font-size:12px!important} .fav-modal .bm-chips{flex:1 1 140px;min-width:0;margin:0!important;padding:0!important} .fav-modal .bm-chips .bm-tbtn{padding:5px 12px!important} .fav-modal .bm-hbtn svg,.fav-modal .bm-del svg,.fav-modal .bm-pin svg{display:block!important;margin:0 auto!important;flex:none} .fav-modal .bm-act-btn svg{display:block!important;margin:0!important;flex:none} .fav-modal .bm-act-btn{justify-content:center;text-align:center} .fav-modal .bm-gear{display:inline-flex!important;align-items:center!important;justify-content:center!important;padding:0!important} .fav-modal .bm-search-clr{position:absolute;inset-inline-end:6px;top:50%;transform:translateY(-50%);display:none;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:50%;background:rgba(255,255,255,.1);color:#ddd;font-size:17px;line-height:1;cursor:pointer} .fav-modal .bm-search-clr:hover{background:rgba(255,255,255,.2);color:#fff} .fav-modal .bm-pin{display:flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;flex-shrink:0;border-radius:8px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);color:#9a9aa2;cursor:pointer;transition:.2s} .fav-modal .bm-pin svg{width:15px;height:15px;margin:0;display:block;vertical-align:baseline} .fav-modal .bm-pin:hover{background:rgba(245,197,24,.18);color:#f5c518} .fav-modal .bm-pin.on{color:#f5c518;background:rgba(245,197,24,.14);border-color:rgba(245,197,24,.4)} .fav-modal .bm-pin.on svg{fill:currentColor} .fav-modal .bm-item.pinned{border-color:rgba(245,197,24,.3)} .fav-modal .bm-gallery-mode .bm-pin{position:absolute;top:6px;right:38px;order:2;z-index:2;width:26px;height:26px;border-radius:6px;background:rgba(0,0,0,.55);border-color:rgba(255,255,255,.1)} .fav-modal .bm-gallery-mode .bm-pin.on{border-color:rgba(245,197,24,.6)} .fav-modal .bm-gallery-mode .bm-link>div{width:calc(100% - 66px);max-width:calc(100% - 66px)} .fav-modal .bm-gallery-mode .bm-grp-btn{order:5;width:100%;padding:5px 8px;font-size:12px;min-width:0} .fav-modal .bm-gallery-mode .bm-grp-btn>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .stats,.panel .stats{width:42px;min-width:42px;max-width:42px;overflow:hidden;white-space:nowrap} .bm-spd:disabled{cursor:default} .fav-modal .bm-setrow-spd{flex-wrap:wrap;padding:6px 0}.fav-modal .bm-setrow-spd>span{flex:1 0 100%}.fav-modal .bm-spdseg{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;width:100%;padding:4px;border-radius:12px;background:rgba(0,0,0,.28)}.fav-modal .bm-spdopt{height:38px;border:0;border-radius:9px;background:transparent;color:inherit;font-size:14px;cursor:pointer}.fav-modal .bm-spdopt.on{background:var(--pt-ac,#d0123f);color:var(--pt-on,#fff);font-weight:600} :host([data-rtl]) .bm-overlay{direction:rtl} `; const CSS_COMPACT = `.fav-modal .bm-hdr{padding-top:6px!important;padding-bottom:6px!important;padding-inline-end:46px!important} .fav-modal .bm-close{top:10px!important;inset-inline-end:4px!important;width:32px!important;height:32px!important;min-width:0!important;min-height:0!important} .fav-modal .bm-tabs .bm-tbtn,.fav-modal .bm-hdr .bm-tabs .bm-tbtn{height:40px!important;min-height:0!important} .fav-modal .bm-tools .bm-hbtn,.fav-modal .bm-tools .bm-tbtn{height:32px!important;min-height:0!important} .fav-modal .bm-tools .bm-hbtn{min-width:38px!important} .fav-modal .bm-seg .bm-tbtn,.fav-modal .bm-seg.bm-wseg .bm-tbtn,.fav-modal .mv-pane>.bm-seg.mv-seg .bm-tbtn{height:30px!important;min-height:0!important} .fav-modal .mv-pane>.bm-seg.mv-seg{min-height:0!important} .fav-modal .bm-search-wrap>.bm-sort-wrap .bm-hbtn{height:34px!important;min-height:0!important} .fav-modal .mv-bar .bm-tbtn,.fav-modal .mv-bar .bm-hbtn{height:34px!important;min-height:0!important} .fav-modal .mv-bar .bm-hbtn{min-width:38px!important} .fav-modal .mv-bar{padding-bottom:6px!important} .fav-modal .bm-search-wrap .bm-srch,.fav-modal .bm-search-wrap input{min-height:0!important} .fav-modal .bm-more{display:none!important} .fav-modal .bm-mpop{display:none;position:absolute;right:8px;bottom:54px;z-index:5;min-width:124px;max-width:calc(100% - 16px);padding:4px;flex-direction:column;gap:2px;border-radius:12px;background:var(--pt-cd,#1b1b21);border:1px solid var(--pt-ln,rgba(255,255,255,.12));box-shadow:0 6px 18px rgba(0,0,0,.5)} .fav-modal .bm-mpop.open{display:flex} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-mpop{display:none!important} .fav-modal .bm-mrow{display:flex;align-items:center;gap:8px;height:36px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:#fff;font-size:13px;text-align:start;cursor:pointer;white-space:nowrap} .fav-modal .bm-mrow:active{background:rgba(255,255,255,.1)} .fav-modal .bm-mrow svg{width:16px;height:16px;flex:none;margin:0} .fav-modal .bm-mrow-del{color:#ff9aa6}`; const PT_SEL = ':host .bm-overlay.fav-ov .bm-modal.fav-modal'; const PT_CTL = 'height:44px!important;min-height:0!important;min-width:0!important;padding:0!important;display:inline-flex!important;align-items:center;justify-content:center;gap:6px;border-radius:var(--pt-r)!important;border:1px solid var(--pt-ln)!important;background:var(--pt-cd)!important;color:var(--pt-tx)!important;box-shadow:none!important;font-size:12px!important;font-weight:500!important'; const PT_ON = 'background:var(--pt-ac)!important;color:var(--pt-on)!important;border-color:transparent!important;font-weight:600!important;box-shadow:none!important'; const PT_CHIP = 'height:36px!important;min-height:0!important;padding:0 12px!important;display:inline-flex!important;align-items:center;justify-content:center;border-radius:999px!important;border:1px solid var(--pt-ln)!important;background:transparent!important;color:var(--pt-dm)!important;font-size:12px!important;font-weight:500!important;box-shadow:none!important'; const PT_CHIP_ON = 'color:var(--pt-on)!important;border-color:transparent!important;background:var(--pt-ac)!important;font-weight:600!important;box-shadow:none!important'; const PT_BACK = ';background:rgba(255,255,255,.05)!important;border-radius:14px!important;padding:4px!important;gap:4px!important'; const PT_ROW = 'display:flex!important;gap:6px!important;border:0!important;border-radius:0!important;background:transparent!important;box-shadow:none!important;margin:0!important;padding:0!important'; const PT_RULES = [ ['', 'display:flex;flex-direction:column;--pt-bg:#0e0f12;--pt-cd:#17191d;--pt-ln:#2a2d33;--pt-tx:#fff;--pt-dm:#fff;--pt-ac:var(--th);--pt-on:#fff;--pt-rd:#ff7a88;--pt-r:12px;background:var(--pt-bg)!important;color:var(--pt-tx);border:0!important;border-radius:0!important;box-shadow:none!important;font-family:Onest,system-ui,-apple-system,sans-serif'], [' .bm-hdr', 'order:90;position:static!important;display:flex!important;flex-wrap:nowrap!important;align-items:center;gap:6px!important;padding:6px 8px calc(8px + env(safe-area-inset-bottom,0px))!important;background:var(--pt-cd)!important;border:0!important;border-top:1px solid var(--pt-ln)!important;flex:none'], [' .bm-hdr .bm-title', 'flex:1 1 auto!important;min-width:0;display:block!important;padding:0!important;margin:0!important'], [' .bm-hdr .bm-tabs', PT_ROW + ';width:100%!important;max-width:100%!important;gap:2px!important;min-height:0!important;overflow:visible!important'], [' .bm-hdr .bm-tabs .bm-tbtn', 'position:relative;flex:1 1 0!important;width:0!important;height:52px!important;min-height:0!important;display:flex!important;flex-direction:column!important;align-items:center;justify-content:center;gap:4px!important;padding:4px 0 0!important;border:0!important;border-radius:var(--pt-r)!important;background:transparent!important;color:var(--pt-dm)!important;font-size:11px!important;font-weight:500!important;box-shadow:none!important'], [' .bm-hdr .bm-tabs .bm-tbtn.on', 'color:var(--pt-ac)!important;background:transparent!important'], [" .bm-hdr .bm-tabs .bm-tbtn::before", "content:'';position:absolute;top:-6px;left:50%;width:32px;height:3px;margin-left:-16px;border-radius:0 0 3px 3px;background:transparent"], [' .bm-hdr .bm-tabs .bm-tbtn.on::before', 'background:var(--pt-ac)'], [' .bm-hdr .bm-tabs .bm-tbtn svg', 'width:20px!important;height:20px!important;margin:0!important'], [' .bm-hdr .bm-tabs .bm-tl', 'display:block!important;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'], [' .bm-hdr .bm-close', 'position:static!important;inset:auto!important;top:auto!important;transform:none!important;flex:none;width:44px!important;height:44px!important;margin:0!important;border-radius:var(--pt-r)!important;background:var(--pt-bg)!important;border:1px solid var(--pt-ln)!important;color:var(--pt-dm)!important'], [' .bm-search-wrap', 'order:1;padding:10px 12px 0!important'], [' .bm-tags-wrap', 'order:2;padding:8px 12px!important;border:0!important;background:transparent!important;gap:6px!important'], [' .bm-list', 'order:3;flex:1 1 auto!important;min-height:0;padding:4px 12px 12px!important;gap:12px!important;grid-auto-rows:max-content!important;align-content:start'], [' .bm-foot', 'order:70;padding:8px 12px!important;background:var(--pt-bg)!important;border:0!important'], [' .bm-tbar', 'order:80;display:flex;flex-direction:column-reverse;gap:8px;padding:8px 12px!important;background:var(--pt-bg)!important;border:0!important;border-top:1px solid var(--pt-ln)!important'], [' .bm-tbar .bm-tools', 'display:flex!important;width:100%!important;gap:6px!important;margin:0!important;justify-content:stretch!important;overflow:visible!important'], [' .bm-tbar .bm-tools>*', 'flex:1 1 0!important;min-width:0!important'], [' .bm-tbar .bm-tools>.bm-wseg-host', 'flex:2 1 0!important;display:flex!important'], [' .bm-tbar .bm-wseg', PT_ROW + ';flex:1;width:100%!important;max-width:none!important'], [' .bm-tbar .bm-hbtn, .bm-tbar .bm-wseg .bm-tbtn', PT_CTL + ';flex:1 1 0'], [' .bm-tbar .bm-hbtn.on, .bm-tbar .bm-wseg .bm-tbtn.on', PT_ON], [' .bm-tbar .bm-gear, .mv-bar .bm-gear', 'border-color:var(--pt-ac)!important;color:#fff!important'], [' .bm-tbar .bm-gear.on, .mv-bar .bm-gear.on', PT_ON], [' .bm-setp, .mv-setp', 'background:var(--pt-cd)!important;border:1px solid var(--pt-ln)!important;border-radius:var(--pt-r)!important'], [' .bm-setp', 'flex:0 0 auto!important;align-self:stretch'], [' .bm-setrow-av', 'margin-top:10px!important;padding-top:6px!important'], [' .bm-setrow-av .bm-avbtn', 'flex:none!important;width:72px!important;height:40px!important;border-radius:10px!important'], [' .bm-setp, .mv-setp', 'background:transparent!important;border:0!important;border-radius:0!important;padding:0!important;gap:8px!important'], [' .bm-setrow', 'min-height:48px!important;padding:0 10px 0 12px!important;gap:10px!important;border:1px solid var(--pt-ln)!important;border-radius:12px!important;background:#22252b!important;background:color-mix(in srgb,var(--th) 14%,#22252b)!important;border-color:color-mix(in srgb,var(--th) 38%,#3a3e46)!important;color:#fff!important;font-size:14px!important;line-height:1.25'], [' .bm-setrow+.bm-setrow', 'border-top:1px solid var(--pt-ln)!important;border-top-color:color-mix(in srgb,var(--th) 38%,#3a3e46)!important'], [' .bm-setrow-spd', 'padding:8px 10px 8px 12px!important'], [' .bm-setrow-av', 'margin-top:0!important;padding-top:0!important'], [' .bm-setrow>.bm-tbtn, .bm-setrow>.bm-hbtn, .bm-setrow .bm-avbtn', 'flex:none!important;width:auto!important;min-width:44px!important;height:36px!important;min-height:0!important;padding:0 12px!important;border-radius:10px!important;border:1px solid var(--pt-ln)!important;border-color:color-mix(in srgb,var(--th) 38%,#3a3e46)!important;background:#0e0f12!important;background:color-mix(in srgb,var(--th) 7%,#0b0c0f)!important;color:#fff!important;font-size:13px!important;font-weight:500!important'], [' .bm-search-in', 'height:44px!important;border-radius:var(--pt-r)!important;background:var(--pt-cd)!important;border:1px solid var(--pt-ln)!important;color:var(--pt-tx)!important'], [' .bm-search-wrap>.bm-sort-wrap', 'height:44px!important;border-radius:var(--pt-r)!important;border:1px solid var(--pt-ln)!important;background:var(--pt-cd)!important'], [' .bm-search-wrap>.bm-sort-wrap .bm-hbtn', 'height:44px!important;background:transparent!important;border-color:var(--pt-ln)!important;color:var(--pt-tx)!important'], [' .bm-tags-wrap .bm-seg', PT_ROW + ';flex:1 1 100%' + PT_BACK], [' .bm-tags-wrap .bm-chips', 'flex:1 1 100%!important' + PT_BACK], [' .bm-tags-wrap .bm-seg .bm-tbtn, .bm-chips .bm-tbtn, .mv-pane>.bm-seg .bm-tbtn', PT_CHIP + ';flex:1 1 auto'], [' .bm-tags-wrap .bm-seg .bm-tbtn.on, .bm-chips .bm-tbtn.on, .mv-pane>.bm-seg .bm-tbtn.on', PT_CHIP_ON], [' .bm-list:not(.bm-gallery-mode) .bm-item', 'background:var(--pt-cd)!important;border:1.5px solid var(--st,var(--pt-ln))!important;border-radius:16px!important;padding:12px!important;box-shadow:none!important'], [' .bm-list:not(.bm-gallery-mode) .bm-item::before', 'display:none'], [' .bm-gallery-mode .bm-item', 'display:grid!important;grid-template-columns:minmax(0,1fr) auto auto auto;align-content:start;gap:0!important;padding:0!important;overflow:clip;align-self:start;min-height:min-content!important;background:var(--pt-cd)!important;border:1.5px solid var(--st,var(--pt-ln))!important;border-radius:20px!important;box-shadow:none!important'], [' .bm-gallery-mode .bm-item .bm-avatar, .bm-gallery-mode .bm-item .bm-prev', 'grid-column:1/-1;grid-row:1;width:100%!important;height:auto!important;aspect-ratio:16/9;object-fit:cover;margin:0!important;border:0!important;border-radius:0!important;transform:none!important'], [' .bm-gallery-mode .bm-item .bm-nwrap', 'display:contents!important'], [' .bm-gallery-mode .bm-item .bm-name', 'grid-column:1;grid-row:2;align-self:start;min-width:0;margin:0!important;padding:10px 0 10px 14px!important;font-size:16px!important;font-weight:700!important;line-height:40px;color:var(--pt-tx)!important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'], [' .bm-gallery-mode .bm-item .bm-seen', 'position:static!important;grid-column:1/-1;grid-row:1;align-self:end;margin:0 10px 10px!important;padding:3px 9px!important;border-radius:999px!important;z-index:2;font-size:12px'], [' .bm-gallery-mode .bm-item .bm-view', 'position:static!important;grid-column:2;grid-row:2;align-self:start;display:inline-flex;align-items:center;gap:5px;height:40px;margin:10px 0 0 8px!important;padding:0!important;border-radius:0!important;background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;color:#fff!important;font-size:14px;font-weight:600'], [' .bm-gallery-mode .bm-item .bm-view svg', 'width:16px;height:16px'], [' .bm-gallery-mode .bm-item .bm-seen', 'justify-self:start'], [' .bm-gallery-mode .bm-item .bm-topic', 'grid-column:1;grid-row:3;align-self:start;margin:-12px 0 0!important;padding:0 8px 12px 14px!important;border:0!important;border-radius:0!important;background:transparent!important;color:var(--pt-dm)!important;font-size:13px!important;line-height:1.35'], [' .bm-gallery-mode .bm-item .bm-pin, .bm-gallery-mode .bm-item .bm-del', 'position:static!important;grid-row:2;align-self:start;width:40px!important;height:40px!important;margin:10px 0 0!important;border:0!important;border-radius:var(--pt-r)!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important'], [' .bm-gallery-mode .bm-item .bm-pin', 'grid-column:3;margin-inline:8px 0!important;background:var(--pt-bg)!important;color:#fff!important'], [' .bm-gallery-mode .bm-item .bm-pin.on', 'color:#ffc83d!important;background:var(--pt-bg)!important'], [' .bm-gallery-mode .bm-item .bm-del', 'grid-column:4;margin-inline:8px 12px!important;background:rgba(255,122,136,.18)!important;color:#fff!important'], [' .bm-gallery-mode .bm-item .bm-actions-row', 'grid-column:1/-1;grid-row:4;display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px!important;margin:0!important;padding:0 12px 12px!important'], [' .bm-gallery-mode .bm-item .bm-act-btn', 'height:42px!important;border-radius:var(--pt-r)!important;background:var(--pt-bg)!important;border:1px solid var(--pt-ln)!important;color:var(--pt-tx)!important'], [' .bm-list:not(.bm-gallery-mode) .bm-act-btn', 'height:40px!important;border-radius:var(--pt-r)!important;background:var(--pt-bg)!important;border:0!important;color:#fff!important'], [' .bm-list:not(.bm-gallery-mode) .bm-pin', 'width:36px!important;height:36px!important;border-radius:10px!important;background:var(--pt-bg)!important;border:0!important;color:#fff!important'], [' .bm-list:not(.bm-gallery-mode) .bm-pin.on', 'color:#ffc83d!important'], [' .bm-list:not(.bm-gallery-mode) .bm-del', 'width:36px!important;height:36px!important;border-radius:10px!important;background:rgba(255,122,136,.18)!important;border:0!important;color:#fff!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item', 'grid-template-columns:minmax(0,1fr) auto!important;border-radius:16px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-name', 'grid-column:1!important;grid-row:2!important;padding:8px 0 0 10px!important;font-size:14px!important;line-height:20px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item.pinned .bm-name::after', "content:'\\2605';color:#ffc83d;margin-inline-start:5px;font-size:12px"], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-view', 'grid-column:2!important;grid-row:2!important;height:20px!important;margin:8px 10px 0 6px!important;gap:4px!important;font-size:13px!important;align-self:start!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-view svg', 'width:15px!important;height:15px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-pin', 'display:none!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-del', 'display:none!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-topic', 'grid-column:1/-1!important;grid-row:3!important;margin:0!important;padding:6px 10px 0!important;font-size:12px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-actions-row', 'grid-column:1/-1!important;grid-row:4!important;gap:5px!important;padding:8px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-act-btn', 'height:36px!important;border-radius:10px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-act-btn:nth-child(3)', 'display:none!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-act-btn.bm-more', 'display:flex!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-act-btn.bm-more.on', 'background:var(--pt-cd)!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item .bm-pin', 'display:none!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item .bm-del', 'display:none!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item .bm-act-btn:nth-child(3)', 'display:none!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item .bm-act-btn.bm-more', 'display:flex!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item .bm-act-btn.bm-more.on', 'background:var(--pt-cd)!important'], [' .bm-gallery-mode .bm-item .bm-act-btn svg', 'width:16px!important;height:16px!important'], [' .mv-tb button svg', 'width:16px!important;height:16px!important'], [' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item .bm-act-btn svg', 'width:20px!important;height:20px!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-name', 'grid-column:1!important;grid-row:1!important;align-self:end!important;position:relative;z-index:2;display:block!important;box-sizing:border-box;margin:0!important;padding:0 0 2px 12px!important;font-size:15px!important;line-height:20px!important;color:#fff!important;background:none!important;text-shadow:0 1px 3px rgba(0,0,0,.7)'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-view', 'grid-column:2!important;grid-row:1!important;align-self:end!important;position:relative!important;bottom:auto!important;right:auto!important;top:auto!important;left:auto!important;z-index:2;display:inline-flex!important;align-items:center!important;gap:4px!important;box-sizing:border-box;height:auto!important;margin:0!important;padding:0 12px 2px 8px!important;font-size:13px!important;line-height:20px!important;color:#fff!important;background:none!important;text-shadow:0 1px 3px rgba(0,0,0,.7)'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-view svg', 'width:14px!important;height:14px!important;margin:0'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-view:empty', 'display:none!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-seen', 'grid-column:1/-1!important;grid-row:1!important;align-self:start!important;justify-self:start!important;margin:10px!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-topic', 'grid-row:2!important;grid-column:1/-1!important;margin:0!important;padding:10px 14px 0!important'], [' .bm-list.bm-gallery-mode.bm-c1 .bm-item' + ' .bm-actions-row', 'grid-row:3!important;gap:6px!important;padding:8px 10px 10px!important'] , [' .bm-add', 'height:48px;border-radius:var(--pt-r)!important;background:var(--pt-ac)!important;color:var(--pt-on)!important;box-shadow:none!important'], ['.mode-mv .mv-pane', 'padding:8px 0 0!important;gap:8px!important;background:transparent'], [' .mv-pane>.bm-seg', 'order:1;' + PT_ROW + ';margin:0 12px!important;width:auto!important;overflow-x:auto!important;scrollbar-width:none' + PT_BACK], [' .mv-pane>.mv-stage', 'order:2'], [' .mv-pane>.mv-setp', 'order:3;margin:0 12px!important'], [' .mv-pane>.mv-bar', 'order:4;margin:0!important;padding:8px 12px!important;gap:6px!important;background:var(--pt-bg);border:0!important;border-top:1px solid var(--pt-ln)!important'], [' .mv-bar .bm-hbtn, .mv-bar .bm-tbtn', PT_CTL], [' .mv-bar .bm-hbtn.on, .mv-bar>.bm-tbtn:first-child', PT_ON], [' .mv-bar .bm-gear', 'border-color:var(--pt-ac)!important;color:#fff!important'], [' .mv-bar .bm-gear.on', PT_ON], [' .mv-bar .mv-clr', 'color:var(--pt-rd)!important;border-color:rgba(255,122,136,.4)!important;background:transparent!important'], [' .mv-grid', 'padding:0 12px 12px!important;gap:12px!important'], [' .mv-tile', 'background:var(--pt-cd)!important;border:1.5px solid var(--pt-ln)!important;border-radius:16px!important;box-shadow:none!important'], [' .mv-tile.act', 'border-color:var(--pt-ac)!important;box-shadow:none!important'], [' .mv-tile:fullscreen, .mv-tile.act:fullscreen, .mv-tile:-webkit-full-screen, .mv-tile.act:-webkit-full-screen', 'border:0!important;border-radius:0!important;outline:0!important;box-shadow:none!important;background:#000!important'], [' .mv-lbl', 'background:transparent!important;border:0!important;padding:10px 12px!important;font-size:15px!important;font-weight:600!important'], [' .mv-vw', 'background:var(--pt-bg)!important;color:var(--pt-dm)!important'], [' .mv-tile .mv-stg', 'order:1'], [' .mv-tile .mv-lbl', 'order:2;position:relative;z-index:3;box-sizing:border-box;height:40px!important;margin-top:-40px!important;align-items:flex-end!important;padding:0 12px 2px!important;font-size:15px!important;line-height:20px!important;color:#fff!important;border:0!important;transition:opacity .15s;background:none!important;text-shadow:0 1px 3px rgba(0,0,0,.7)'], [' .mv-tile .mv-vw', 'padding:0!important;gap:4px!important;border-radius:0!important;background:none!important;color:#fff!important;font-size:13px!important;font-weight:600!important;line-height:20px!important'], [' .mv-tile .mv-vw svg', 'width:14px!important;height:14px!important'], [' .mv-tile .mv-nm', 'min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'], [' .mv-tile .mv-tb', 'order:3'], [' .mv-tile.act.ui .mv-lbl', 'opacity:0'], [' .mv-grid.multi .mv-tile .mv-lbl', 'height:30px!important;margin-top:-30px!important;padding:0 8px 2px!important;font-size:13px!important;line-height:18px!important'], [' .mv-grid.multi .mv-tile .mv-vw', 'font-size:12px!important'], [' .mv-grid.multi .mv-tile .mv-vw svg', 'width:13px!important;height:13px!important'], [' .mv-tb', 'background:transparent!important;border:0!important;padding:8px 10px 10px!important;gap:6px!important'], [' .mv-tb button', 'height:42px!important;border-radius:var(--pt-r)!important;background:var(--pt-bg)!important;border:1px solid var(--pt-ln)!important;color:var(--pt-tx)!important'], [' .mv-grid:not(.multi) .mv-tile.act .mv-tb button:nth-child(3)', 'background:var(--pt-bg)!important;border-color:var(--pt-ln)!important;color:var(--pt-rd)!important'], [' .cal-pane', 'padding:10px 12px 0!important;gap:10px!important;background:transparent'], [' .cal-pane .cal-filters>.cal-frow:first-child', 'display:flex!important;flex-wrap:nowrap!important;gap:6px!important;align-items:center'], [' .cal-pane .cal-filters .bm-seg', PT_ROW + ';flex:none'], [' .cal-pane .cal-filters .bm-seg .bm-tbtn', PT_CTL + ';width:48px!important'], [' .cal-pane .cal-filters .bm-seg .bm-tbtn.on', PT_ON], [' .cal-pane .cal-filters .bm-seg .bm-tbtn>span', 'display:none!important'], [' .cal-pane .cal-pill', PT_CTL + ';flex:1 1 auto!important'], [' .cal-pane .cal-dd', PT_CTL + ';padding:0 12px!important'], [' .cal-pane .bm-tbtn.on, .cal-pane .cal-pill.on, .cal-pane .cal-dd.on, .cal-pane .cal-opt.on, .cal-pane .cal-mrow.on', PT_ON], [' .cal-pane .cal-day', 'background:var(--pt-cd)!important;border:1px solid var(--pt-ln)!important;border-radius:12px!important;color:var(--pt-tx)!important;font-weight:600;box-shadow:none!important'], [' .cal-pane .cal-day.past', 'color:var(--pt-dm)!important'], [' .cal-pane .cal-day.today', 'border-color:var(--pt-ac)!important'], [' .cal-pane .cal-day.sel', 'background:var(--pt-ac)!important;color:var(--pt-on)!important;border-color:transparent!important'], [' .cal-pane .cal-b', 'background:rgba(255,255,255,.12)!important;background:color-mix(in srgb,var(--pt-ac) 24%,transparent)!important;color:var(--pt-tx)!important'], [' .cal-pane .cal-b.s', 'background:rgba(90,150,255,.22)!important;color:#a9bcff!important'], [' .cal-pane .cal-day.sel .cal-b', 'background:rgba(0,0,0,.25)!important;color:var(--pt-on)!important'], [' .cal-pane .cal-det', 'flex:1 0 auto;background:var(--pt-cd)!important;border:0!important;border-radius:18px 18px 0 0!important;box-shadow:none!important;margin:0 -12px!important;padding:14px!important'], [' .cal-pane .cal-dock', 'order:5;position:sticky;bottom:0;z-index:3;margin:0 -12px!important;padding:8px 12px!important;gap:8px!important;background:var(--pt-bg);border-top:1px solid var(--pt-ln)'], [' .cal-pane .cal-head', 'display:flex;gap:6px;align-items:center'], [' .cal-pane .cal-title', 'flex:1;text-align:center;font-weight:700;font-size:15px;color:var(--pt-tx)'], [' .cal-pane .cal-nav, .cal-pane .cal-today', PT_CTL + ';min-width:44px!important;padding:0 12px!important'], [' .cal-pane .cal-dock .bm-seg', PT_ROW + ';width:100%!important'], [' .cal-pane .cal-dock .bm-seg .bm-tbtn', PT_CTL + ';flex:1 1 0'], [' .cal-pane .cal-dock .bm-seg .bm-tbtn.on', PT_ON], [' .hs-pane', 'padding:10px 12px!important;gap:8px!important'], [' .hs-pane .hs-row', 'background:var(--pt-cd)!important;border:0!important;border-radius:16px!important;box-shadow:none!important'], [' .hs-pane .hs-btn', 'width:40px!important;height:40px!important;border-radius:var(--pt-r)!important;background:var(--pt-bg)!important;border:1px solid var(--pt-ln)!important;color:#ffc83d!important'], [' .hs-pane .hs-btn.del', 'color:var(--pt-rd)!important;background:rgba(255,122,136,.12)!important'], [' .hs-pane .bm-tbtn', PT_CHIP], [' .hs-pane .bm-tbtn.on', PT_CHIP_ON], [' .hs-pane .hs-trash', 'color:var(--pt-rd)!important'], [' .hs-pane .hd-hint, .hs-pane .bm-empty, .bm-list .bm-empty', 'color:#fff!important'], [' .hs-pane .ch-bar, .hs-pane .ch-fill', 'background:var(--pt-ac)!important'] ]; const CSS_CALDOCK = '.cal-dock{order:-1;display:flex;flex-direction:column;gap:10px}\n'; const PT_DYN = `@supports(color:color-mix(in srgb,red 10%,blue)){${PT_SEL}{--pt-bg:color-mix(in srgb,var(--th) 7%,#0b0c0f);--pt-cd:color-mix(in srgb,var(--th) 11%,#15171b);--pt-ln:color-mix(in srgb,var(--th) 24%,#24272d);--pt-dm:#fff}} @supports(color:oklch(from red l c h)){${PT_SEL}{--pt-on:oklch(from var(--th) calc((0.7 - l) * 1000) 0 0 / 1)}} `; const PT_WIDE = `@media(min-width:521px){${PT_SEL}{border:1px solid var(--pt-ln)!important;border-radius:22px!important;overflow:hidden}} @media(min-width:760px){${[ [' .bm-hdr', 'order:-1;border-top:0!important;border-bottom:1px solid var(--pt-ln)!important;padding:8px 12px!important'], [' .bm-hdr .bm-tabs .bm-tbtn', 'height:44px!important;flex-direction:row!important;gap:8px!important;padding:0!important'], [' .bm-hdr .bm-tabs .bm-tbtn::before', 'top:auto;bottom:-8px;border-radius:3px 3px 0 0'], [' .bm-tbar', 'border-top:1px solid var(--pt-ln)!important'] ].map(([x, d]) => x.split(',').map(y => PT_SEL + y).join(',') + `{${d}}`).join('\n')}} `; const CSS_PULT_PANEL = `:host .panel:not(.mini){--pb:#0e0f12;--pc:#17191d;--pl:#2a2d33;background:var(--pb)!important;border:1px solid var(--pl)!important;border-radius:18px!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;box-shadow:0 14px 36px rgba(0,0,0,.6)!important} @supports(color:color-mix(in srgb,red 10%,blue)){:host .panel:not(.mini){--pb:color-mix(in srgb,var(--th) 7%,#0b0c0f);--pc:color-mix(in srgb,var(--th) 11%,#15171b);--pl:color-mix(in srgb,var(--th) 24%,#24272c)}} :host .panel:not(.mini)::before{display:none!important} :host .panel:not(.mini){padding:8px!important} :host .panel .col{gap:6px!important} :host .panel .sep{display:none!important} :host .panel .hdr,:host .panel .grid-4,:host .panel .row-4,:host .panel .row-2{background:rgba(255,255,255,.05)!important;border:0!important;border-radius:14px!important;padding:4px!important;margin:0!important;gap:6px!important} :host .panel .btn{height:56px!important;min-height:56px!important;max-height:56px!important;box-sizing:border-box;overflow:hidden;padding:8px 2px 6px!important;border-radius:12px!important;border:1px solid var(--pl)!important;background:transparent!important;color:#fff!important;transform:none!important} :host .panel .btn:not(.fav-on){color:#fff!important} :host .panel .btn:hover,:host .panel .btn:not(.on):hover{background:rgba(255,255,255,.08)!important;border-color:var(--pl)!important;transform:none!important} :host .panel .btn:active{transform:scale(.97)!important} :host .panel .btn sub{font-size:10.5px!important;font-weight:500;letter-spacing:.2px;opacity:1!important;color:#fff;white-space:nowrap;word-break:normal;max-width:none} :host .panel .btn.fav-on sub{color:#fff} :host .panel .btn.on{background:var(--th)!important;border-color:transparent!important;box-shadow:none;font-weight:600} :host .panel .h-btn{border-radius:12px!important;background:transparent!important;border:1px solid var(--pl)!important;color:#fff!important} :host .panel .h-btn:hover{background:rgba(255,255,255,.1)!important;transform:none!important} :host .panel .h-btn.close:hover{background:rgba(231,76,60,.9)!important} :host .panel .h-btn.lang{border-radius:12px!important} :host .panel .stats{background:transparent!important;border:0!important;color:#fff!important} :host .panel .drag{color:#fff!important} :host .panel .hdr-btns .hsep{background:var(--pl)!important} :host .panel .hdr-btns{flex:1 1 auto!important;min-width:0} :host .panel .hdr-btns .stats{flex:1 1 0!important;width:auto!important;min-width:62px!important;max-width:none!important;height:30px!important;justify-content:center!important;align-items:center!important;padding:0 6px!important;margin:0!important} :host .panel .v-cap{background:var(--pc)!important;border:1px solid var(--pl)!important;border-radius:14px!important} :host .panel .vval,:host .panel .s-val{color:#fff!important} :host .panel .z-btn{border-radius:12px!important;background:transparent!important;border:1px solid var(--pl)!important;color:#fff!important} :host .panel .z-btn:hover{background:rgba(255,255,255,.1)!important} :host .panel .z-btn.on{background:var(--th)!important;border-color:transparent!important} :host .panel .scale-ctrl{background:transparent!important;border:1px solid var(--pl);border-radius:12px!important;padding:2px 6px!important} :host .panel .scale-ctrl .z-btn{width:24px;height:24px!important;border:0!important;border-radius:8px!important;background:rgba(255,255,255,.08)!important} :host .panel .hotkeys{color:#fff!important;font-size:11px!important;opacity:1;font-weight:500} :host .panel:not(.mini){--pon:#fff} @supports(color:oklch(from red l c h)){:host .panel:not(.mini){--pon:oklch(from var(--th) calc((0.7 - l) * 1000) 0 0 / 1)}} :host .panel .btn,:host .panel .z-btn,:host .panel .h-btn{background:var(--pc)!important;font-weight:500} :host .panel .btn:hover,:host .panel .z-btn:hover,:host .panel .h-btn:hover{background:color-mix(in srgb,var(--pc) 80%,#fff)!important} :host .panel .btn.on,:host .panel .z-btn.on{background:var(--th)!important;color:var(--pon)!important;border-color:transparent!important;font-weight:600!important} :host .panel .btn.on sub{color:var(--pon)!important} :host .panel .h-btn.lang{border-color:var(--th)!important;color:#fff!important} :host .panel .h-btn.close{color:#fff!important} :host .panel .h-btn.close:hover{background:rgba(255,122,136,.9)!important} :host .panel .scale-ctrl{background:var(--pc)!important} :host .panel .scale-ctrl .z-btn{background:rgba(255,255,255,.08)!important;border:0!important} :host .panel .vslider{height:4px!important;border-radius:2px!important} :host .panel .vslider::-webkit-slider-thumb{width:18px!important;height:18px!important;border:0!important;box-shadow:none!important;background:#fff!important} :host .panel .btn:focus-visible,:host .panel .z-btn:focus-visible,:host .panel .h-btn:focus-visible{outline:2px solid var(--th)!important;outline-offset:2px} :host .panel .btn.fav-on{color:#ffc83d!important;border-color:rgba(255,200,61,.6)!important;background:rgba(255,200,61,.1)!important;position:relative} :host .panel .btn.fav-on sub{color:#fff!important} :host .panel:not(.mini){--pl:#3a3e46;--pt:#22252b} @supports(color:color-mix(in srgb,red 10%,blue)){:host .panel:not(.mini){--pl:color-mix(in srgb,var(--th) 38%,#3a3e46);--pt:color-mix(in srgb,var(--th) 14%,#22252b)}} :host .panel .hdr,:host .panel .grid-4,:host .panel .row-4,:host .panel .row-2{background:rgba(0,0,0,.34)!important} :host .panel .btn,:host .panel .z-btn,:host .panel .h-btn{background:var(--pt)!important;border-color:var(--pl)!important;transition:background .12s,border-color .12s,transform .12s} :host .panel .scale-ctrl{background:var(--pt)!important;border-color:var(--pl)!important} :host .panel .v-cap{background:rgba(0,0,0,.34)!important;border-color:var(--pl)!important} :host .panel .btn:hover,:host .panel .z-btn:hover,:host .panel .h-btn:hover{background:color-mix(in srgb,var(--pt) 85%,#fff)!important} :host .panel .btn:not(.on):active,:host .panel .z-btn:not(.on):active,:host .panel .h-btn:active{background:color-mix(in srgb,var(--th) 38%,var(--pt))!important;border-color:var(--th)!important;transform:scale(.95)!important} :host .panel .btn.on:active,:host .panel .z-btn.on:active{filter:brightness(.85);transform:scale(.95)!important} :host .panel .scale-ctrl .z-btn{background:rgba(255,255,255,.1)!important;border:0!important}`; const CSS_PULT_SQ = [ ' .bm-item', ' .bm-list:not(.bm-gallery-mode) .bm-item', ' .bm-gallery-mode .bm-item', ' .bm-list.bm-gallery-mode:not(.bm-c1) .bm-item', ' .bm-list.bm-gallery-mode.bm-c1 .bm-item', ' .mv-tile', ' .mv-tile:fullscreen' ].map(x => PT_SEL + x).join(',') + '{border-radius:4px!important}\n' + [' .mv-tile .mv-stg', ' .mv-tile video', ' .bm-item .bm-avatar', ' .bm-item .bm-prev', ' .bm-item img[data-frame]'].map(x => PT_SEL + x).join(',') + '{border-radius:0!important}\n' + ':host .panel .grid-4{grid-auto-rows:56px!important}\n' + ':host .panel .grid-4>.btn{width:100%!important;min-width:0!important;height:56px!important;min-height:56px!important;max-height:56px!important;overflow:hidden!important;padding:8px 2px 6px!important}\n' + ':host .panel .grid-4>.btn sub{white-space:nowrap!important;word-break:normal!important;max-width:100%}\n'; const CSS_PULT = CSS_CALDOCK + PT_RULES.map(([x, d]) => x.split(',').map(y => PT_SEL + y).join(',') + `{${d}}`).join('\n') + '\n' + PT_DYN + PT_WIDE + CSS_PULT_PANEL + CSS_PULT_SQ; const CSS_X = `.off{display:none!important} .panel.dragging{-webkit-backdrop-filter:none!important;backdrop-filter:none!important} :host([data-dfb]) :is(.bm-overlay,.cf-ov,.dlg-ov,.fr-ov){position:absolute!important;inset:auto 0 0 0!important;width:100%!important;height:100vh!important;height:100dvh!important} .hd-sec{display:flex;align-items:center;gap:6px} .cf-ov{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(0,0,0,.75);pointer-events:auto} .cf-card{width:320px;max-width:100%;padding:20px;border-radius:18px;background:linear-gradient(180deg,rgb(32,32,40),rgb(18,18,23));border:1px solid rgba(255,255,255,.1);box-shadow:0 20px 60px rgba(0,0,0,.7);color:#eee;font-size:14px;line-height:1.45} .cf-txt{margin-bottom:16px} .cf-btns{display:grid;grid-template-columns:1fr 1fr;gap:8px} .cf-btns button{padding:11px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.07);color:#eee;font-size:13px;font-weight:700;cursor:pointer} .cf-btns .cf-yes{background:var(--th);border-color:transparent;color:#fff} .st-chips{display:flex;flex-wrap:wrap;gap:6px} .st-chips .bm-tbtn{flex:0 0 auto;height:auto;margin:0;padding:7px 12px;font-size:12px;border:1px solid rgba(255,255,255,.2);border-radius:10px;background:rgba(255,255,255,.05);white-space:normal;text-align:center} .st-chips .bm-tbtn.on{background:var(--th)} .bm-view{display:inline-flex;align-items:center;gap:3px;margin-left:8px;font-size:11px;color:#aaa;flex-shrink:0} .bm-view svg{width:12px;height:12px} .hs-tools{display:flex;justify-content:flex-end} .hs-tools .bm-btn{padding:8px 16px;font-size:12px} .hs-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;background:rgba(255,255,255,.05);cursor:pointer;transition:.2s} .hs-row:hover{background:rgba(255,255,255,.09)} .hs-ava{flex:none;width:42px;height:42px;border-radius:50%;object-fit:cover;background:rgba(255,255,255,.1)} .hs-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .hs-name{color:#fff;font-size:14px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .hs-meta{color:#9a9aa2;font-size:11.5px} .hs-btn{flex:none;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:rgba(255,255,255,.08);color:#ddd;cursor:pointer;display:flex;align-items:center;justify-content:center} .hs-btn:hover{background:rgba(255,255,255,.18)} .hs-btn.on{color:#ffc83d} .hs-btn.on svg{fill:currentColor} :where(.bm-modal) button{display:inline-flex;align-items:center;justify-content:center;gap:5px} .bm-modal button svg{flex:none;margin:0} .hs-btn svg,.bm-close svg,.bm-del svg,.bm-pin svg{display:block;margin:0!important} .hs-btn svg{width:16px;height:16px} .bm-btn,.bm-add{display:inline-flex;align-items:center;justify-content:center;gap:6px} .fav-modal .bm-tabs{flex:0 1 auto;min-width:0;max-width:100%} .fav-modal .bm-hdr .bm-title{padding-inline-end:0!important;flex:1 1 auto!important;min-width:0;min-height:0} .fav-modal .bm-tools{order:2!important;flex:0 0 auto!important;margin-inline-start:auto!important;justify-content:flex-end!important;gap:6px!important} .fav-modal .bm-tabs .bm-tbtn{height:34px!important;padding:0 12px!important;font-size:13px!important;font-weight:700;gap:6px;color:#aaa;flex:1 1 auto} .fav-modal .bm-tabs .bm-tbtn.on{color:#fff} .fav-modal .bm-tabs .bm-tbtn svg{width:15px;height:15px} .fav-modal .bm-hdr .bm-tabs{display:flex!important;width:100%!important;flex:1 1 auto!important;min-width:0!important;max-width:100%!important} .fav-modal .bm-hdr .bm-tabs .bm-tbtn{flex:1 1 auto!important;width:auto;min-width:0!important;padding:0 3px!important;gap:3px!important;font-size:12.5px!important;overflow:hidden;justify-content:center} .fav-modal .bm-tabs .bm-tbtn .bm-tl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .fav-modal .bm-tools{order:3!important;flex:1 0 100%!important;justify-content:flex-end!important} .fav-modal .bm-tools .bm-wseg-host{margin-inline-end:auto;display:flex;min-width:0} .fav-modal.mode-hist .bm-search-wrap,.fav-modal.mode-hist .bm-tags-wrap,.fav-modal.mode-hist .bm-list,.fav-modal.mode-hist .bm-foot,.fav-modal.mode-hist .bm-tools,.fav-modal.mode-hist .bm-setp{display:none!important} .fav-modal .hs-pane{display:none} .fav-modal.mode-hist .hs-pane{display:flex;flex-direction:column;gap:8px;flex:1 1 auto;min-height:0;overflow-y:auto;padding:14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .hs-pane>*{flex:none} .hs-btn.del{color:#e5616f;background:rgba(229,97,111,.12)} .hs-btn.del:hover{background:#e74c3c;color:#fff} .st-chips{align-items:center} .st-chips.hs-seg{flex-wrap:nowrap;gap:4px;padding:3px;border-radius:12px;background:rgba(255,255,255,.05)} .st-chips.hs-seg .bm-tbtn{flex:1 1 0;min-width:0;padding:8px 4px;border:0;background:transparent;border-radius:9px;box-shadow:none} .st-chips.hs-seg .bm-tbtn.on{background:var(--th)} .hs-top{justify-content:space-between;flex-wrap:nowrap} .hs-trash{margin-inline-start:auto;flex:none;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;background:transparent;color:#ff8591;border-radius:8px;cursor:pointer} .hs-trash:hover{background:rgba(229,97,111,.18)} .hs-trash svg{width:18px;height:18px;display:block;flex:none;margin:0} .hs-sep{flex:none;width:1px;height:22px;margin:0 6px;background:rgba(255,255,255,.22);border-radius:1px} .ch-box{padding:10px 12px;border-radius:10px;background:rgba(255,255,255,.05);margin-bottom:8px} .ch-ttl{display:flex;justify-content:space-between;gap:8px;font-size:12px;color:rgba(255,255,255,.65);margin-bottom:8px} .ch-ttl b{font-weight:600;color:#fff} .ch-bars{display:flex;align-items:flex-end;gap:3px;height:84px;direction:ltr} .ch-col{flex:1 1 0;min-width:0;height:100%;display:flex;align-items:flex-end} .ch-bar{width:100%;min-height:2px;border-radius:3px 3px 0 0;background:var(--th)} .ch-bar.zero{background:rgba(255,255,255,.12)} .ch-axis{display:flex;gap:3px;margin-top:4px;font-size:9px;color:rgba(255,255,255,.5);direction:ltr} .ch-axis span{flex:1 1 0;min-width:0;text-align:center;white-space:nowrap} .ch-row{display:flex;align-items:center;gap:8px;font-size:12px;margin-top:6px} .ch-name{flex:0 0 32%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .ch-track{flex:1 1 auto;height:8px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden} .ch-fill{height:100%;border-radius:3px;background:var(--th)} .ch-val{flex:0 0 auto;color:rgba(255,255,255,.65);font-variant-numeric:tabular-nums} .bm-view{margin-left:0;margin-inline-start:8px} :host([data-rtl]) .fav-modal .bm-gallery-mode .bm-pin{right:auto;left:auto;inset-inline-end:38px} :host([data-rtl]) .bm-gallery-mode .bm-del{right:auto;left:6px} :host([data-rtl]) .fav-modal .bm-gallery-mode .bm-pin{left:38px} :host([data-rtl]) .fav-modal .bm-gallery-mode .bm-link>div{padding-inline:0} :host([data-rtl]) .fav-modal .bm-item{padding:12px 16px 12px 12px} :host([data-rtl]) .fav-modal .bm-gallery-mode .bm-item{padding:8px} :host([data-rtl]) .fav-modal .bm-item::before{left:auto;right:0;border-radius:2px 0 0 2px} :host([data-rtl]) .fav-modal .bm-topic{border-left:0;border-right:3px solid var(--th)} :host([data-rtl]) .st-val{text-align:left} :host([data-rtl]) .fav-modal .bm-sort-wrap .bm-hbtn+.bm-hbtn{border-left:0;border-right:1px solid rgba(255,255,255,.12)} :host([data-rtl]) .fav-modal .bm-seg .bm-tbtn+.bm-tbtn{border-left:0;border-right:1px solid rgba(255,255,255,.12)} .fav-modal .bm-seg .bm-tbtn{gap:6px} .fav-modal .bm-chips .bm-tbtn svg{width:12px;height:12px;margin-inline-end:4px;vertical-align:-1px} .fav-modal .bm-grp-btn svg{width:12px;height:12px;margin-inline-end:4px;vertical-align:-1px} .dlg-txt{display:flex;align-items:center;gap:8px} .dlg-txt svg{width:14px;height:14px;flex:none} .fav-modal .bm-ptags{display:flex;flex-wrap:wrap;gap:4px;width:100%} .fav-modal .bm-ptag{padding:2px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#cfcfd6;font-size:11px;font-family:inherit;cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .fav-modal .bm-ptag.on{background:var(--th);color:#fff;border-color:transparent} .fav-modal .bm-ptag.more{cursor:default;opacity:.7} .fav-modal .bm-gallery-mode .bm-ptags{order:6} .fav-modal .bm-gallery-mode .bm-ptag{font-size:10px;padding:1px 6px} .fav-modal .bm-list.bm-gallery-mode .bm-item .bm-grp-btn{display:none!important} .fav-modal .bm-gallery-mode .bm-nwrap{flex-wrap:wrap;row-gap:2px;align-content:flex-start} .fav-modal .bm-gallery-mode .bm-nwrap .bm-stat{flex:none;margin:0 6px 0 0} .fav-modal .bm-gallery-mode .bm-nwrap .bm-name{flex:1 0 calc(100% - 16px);min-width:0} .fav-modal .bm-gallery-mode .bm-nwrap .bm-view{margin:0 8px 0 0} .fav-modal .bm-gallery-mode .bm-nwrap .bm-view:not(:empty),.fav-modal .bm-gallery-mode .bm-nwrap .bm-view:empty+.bm-seen{margin-inline-start:15px} .fav-modal .bm-gallery-mode .bm-nwrap .bm-seen{margin-left:0} .fav-modal .bm-tabs .bm-tbtn{padding:0 8px!important;font-size:12.5px!important} .fav-modal .cal-pane{display:none} .fav-modal.mode-cal .cal-pane{display:flex;flex-direction:column;gap:10px;flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px 14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .fav-modal.mode-cal .bm-search-wrap,.fav-modal.mode-cal .bm-tags-wrap,.fav-modal.mode-cal .bm-list,.fav-modal.mode-cal .bm-foot,.fav-modal.mode-cal .bm-tools,.fav-modal.mode-cal .bm-setp,.fav-modal.mode-cal .hs-pane{display:none!important} .fav-modal.mode-hist .cal-pane{display:none!important} .cal-pane{color:#e6e6e6}.cal-pane>*{flex:none} .cal-pane{overflow-anchor:none} .cal-head{display:flex;align-items:center;gap:8px} .cal-title{flex:1;text-align:center;font-weight:700;font-size:15px;color:#f2f2f2} .cal-nav svg{display:block;margin:auto} .cal-nav,.cal-today{height:34px;min-width:34px;padding:0 10px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#eee;font-size:16px;font-family:inherit;cursor:pointer} .cal-today{font-size:12px} .cal-filters{display:flex;flex-direction:column;gap:8px} .cal-frow{display:flex;gap:8px;flex-wrap:wrap;align-items:center} .cal-sel{flex:1 1 130px;min-width:0;height:34px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(0,0,0,.35);color:#eee;padding:0 8px;font:inherit;font-size:13px} .cal-tl{display:inline-flex;align-items:center;gap:6px} .cal-tl svg{width:14px;height:14px;flex:none} .cal-pill{flex:1 1 100%;height:36px;padding:0 12px;text-align:center;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#ddd;font:inherit;font-size:13px;cursor:pointer} .cal-pill.on{background:var(--th);color:#fff;border-color:transparent} .cal-models{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;max-height:280px;overflow-y:auto;padding:8px;border-radius:10px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.06);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .cal-mrow{display:flex;align-items:center;justify-content:flex-start;gap:8px;min-width:0;height:42px;padding:0 10px 0 7px;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.05);color:#d4d4da;font:inherit;font-size:13px;font-weight:600;cursor:pointer;text-align:start} .cal-mrow>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .cal-mrow svg{width:14px;height:14px;flex:none} .cal-mrow.on{background:var(--th);border-color:transparent;color:#fff} .cal-mrow.reset{grid-column:1/-1;justify-content:center;height:34px;font-weight:500;color:#aaa} .cal-mrow.reset>span{flex:none} .cal-ava.sm{width:26px;height:26px} .cal-dd{flex:1 1 130px;min-width:0;height:36px;display:flex;align-items:center;gap:8px;padding:0 12px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#ddd;font:inherit;font-size:13px;cursor:pointer;text-align:start} .cal-dd svg{width:14px;height:14px;flex:none;opacity:.8} .cal-dd>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .cal-dd::after,.cal-fold::after{content:'';flex:none;width:7px;height:7px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg) translateY(-2px);opacity:.6;transition:transform .15s} .cal-dd.open::after,.cal-fold.open::after{transform:rotate(225deg) translateY(-2px)} .cal-dd.on{background:var(--th);color:#fff;border-color:transparent} .cal-panel{display:flex;flex-wrap:wrap;gap:6px;max-height:230px;overflow-y:auto;padding:10px;border-radius:10px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.06);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .cal-panel .cal-sep{flex:1 1 100%;height:0;border-top:1px solid rgba(255,255,255,.08);margin:2px 0} .cal-opt{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:30px;padding:0 11px;border-radius:999px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#cfcfd6;font:inherit;font-size:12.5px;cursor:pointer;white-space:nowrap} .cal-opt svg{width:13px;height:13px;flex:none} .cal-opt>span{overflow:hidden;text-overflow:ellipsis} .cal-opt i{font-style:normal;font-size:10.5px;opacity:.55} .cal-opt.all{font-weight:700} .cal-opt.on{background:var(--th);color:#fff;border-color:transparent} .cal-wsec{display:flex;flex-direction:column;gap:8px} .cal-dtitle+.cal-wsec>.cal-fold{margin-top:0;padding-top:0;border-top:0} .cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px} .cal-wd{text-align:center;font-size:11px;font-weight:600;color:#8a8a90;padding:2px 0;text-transform:capitalize;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .cal-day{position:relative;min-height:54px;padding:7px 2px 5px;border-radius:10px;border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.04);color:#e6e6ea;display:flex;flex-direction:column;align-items:center;justify-content:space-between;gap:4px;cursor:pointer;font:inherit;font-size:13px;font-weight:600;min-width:0;transition:background .15s} .cal-day:hover{background:rgba(255,255,255,.09)} .cal-day.blank{visibility:hidden;pointer-events:none} .cal-day.past{color:#9a9aa2} .cal-day.today{border-color:var(--th);box-shadow:inset 0 0 0 1px var(--th)} .cal-day.sel{background:var(--th);color:#fff;border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)} .cal-n{line-height:1} .cal-bs{display:flex;gap:3px;justify-content:center;align-items:center;min-height:15px} .cal-b{font-style:normal;font-size:10px;font-weight:700;line-height:1;min-width:16px;padding:3px 4px;text-align:center;border-radius:999px;background:rgba(46,204,113,.22);color:#7ee2a8} .cal-b.s{background:rgba(90,150,255,.22);color:#9cc0ff} .cal-day.past .cal-b.s{opacity:.6} .cal-day.sel .cal-b{background:rgba(0,0,0,.32);color:#fff} .cal-det{display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:12px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.06)} .cal-dtitle{font-weight:700;font-size:15px;color:#f2f2f2} .cal-sec{font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:#8a8a90;margin-top:6px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)} .cal-dtitle+.cal-sec{margin-top:0;padding-top:0;border-top:0} .cal-tbl{display:flex;flex-direction:column;border-radius:10px;overflow:hidden;border:1px solid rgba(255,255,255,.07)} .cal-trow{display:grid;grid-template-columns:minmax(0,1fr) 52px 78px 54px;align-items:center;gap:6px;padding:7px 10px;font-size:12.5px;font-variant-numeric:tabular-nums} .cal-trow>span{text-align:end;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#cfcfd6} .cal-trow+.cal-trow{border-top:1px solid rgba(255,255,255,.05)} .cal-trow:nth-child(even){background:rgba(255,255,255,.03)} .cal-thead{background:rgba(255,255,255,.08)!important;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase} .cal-thead>span{color:#9a9aa2} .cal-tfoot{background:rgba(255,255,255,.08)!important;font-weight:700} .cal-tfoot>span{color:#fff} .cal-tfoot>span:first-child{text-align:start} .cal-who{display:flex;align-items:center;gap:8px;min-width:0} .cal-who a{min-width:0;color:#fff;text-decoration:none;font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .cal-thead>span:first-child{text-align:start} .cal-trow>span:nth-child(2),.cal-trow>span:nth-child(3){text-align:center} .cal-trow>span:nth-child(4){text-align:end} .cal-axis{display:flex;justify-content:space-between;margin-left:38px;direction:ltr;font-size:10px;color:#6f6f76;font-variant-numeric:tabular-nums} .cal-row{display:flex;align-items:center;gap:10px;min-width:0} .cal-ava{flex:none;width:28px;height:28px;border-radius:50%;object-fit:cover;background:rgba(255,255,255,.08)} .cal-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px} .cal-line{display:flex;align-items:center;gap:6px;min-width:0;font-size:13px} .cal-line a{flex:1;min-width:0;color:#fff;text-decoration:none;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .cal-line a:hover{color:var(--th)} .cal-t{flex:none;color:#aab;font-size:12px;font-variant-numeric:tabular-nums} .cal-bar{position:relative;height:6px;border-radius:2px;background:rgba(255,255,255,.08);direction:ltr} .cal-seg{position:absolute;top:0;bottom:0;border-radius:2px;background:#5a96ff} .cal-now{position:absolute;top:-3px;bottom:-3px;width:2px;margin-left:-1px;border-radius:1px;background:#fff} .cal-dot{flex:none;width:8px;height:8px;border-radius:50%;background:#2ecc71;box-shadow:0 0 6px #2ecc71} .cal-muted{color:#8a8a90;font-size:12px} .cal-fold{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;margin-top:6px;padding:10px 0 0;border:0;border-top:1px solid rgba(255,255,255,.07);background:none;font-family:inherit;cursor:pointer;text-align:start} .cal-fold>span{flex:1;min-width:0} .fav-modal .bm-list:not(.bm-gallery-mode) .bm-ptags{gap:5px;margin-top:4px;padding-top:8px;border-top:1px solid rgba(255,255,255,.06)} .fav-modal .bm-ptags .bm-ptag{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;border:0;border-radius:999px;background:rgba(255,255,255,.05);color:#9aa0b2;font-size:11.5px} .fav-modal .bm-ptic{width:15px;height:15px;flex:none;object-fit:contain} .fav-modal .bm-chips .bm-tbtn .bm-ptic{width:15px;height:15px;margin-inline-end:5px;vertical-align:-3px} .cal-tic{width:16px;height:16px;flex:none;object-fit:contain} .fav-modal .bm-ptags .bm-ptag.more{background:transparent;padding:2px 4px} .fav-modal .bm-ptags .bm-ptag.on{background:var(--th);color:#fff} .fav-modal.bm-no-ptags .bm-ptags{display:none!important} .fav-modal.bm-no-note .bm-item .bm-note{display:none!important} .fav-modal .bm-chips .bm-chips-sep{flex:none;width:1px;align-self:stretch;margin:4px 2px;background:rgba(255,255,255,.14)}.fav-modal .bm-chips>.bm-tbtn:first-child{position:sticky;left:0;z-index:3;background-color:#1b1b21;background-image:linear-gradient(rgba(255,255,255,.06),rgba(255,255,255,.06));box-shadow:5px 0 6px #1b1b21}.fav-modal .bm-chips>.bm-tbtn:first-child.on{background-image:linear-gradient(var(--th),var(--th))} .fav-modal .bm-gallery-mode .bm-ptags{display:none!important} .fav-modal .bm-gallery-mode .bm-link>div{width:100%;max-width:100%;pointer-events:auto} .fav-modal .bm-gallery-mode .bm-link{position:relative} .fav-modal .bm-gallery-mode .bm-nwrap{align-items:center;flex-wrap:nowrap;row-gap:0;min-height:26px;padding-inline-end:58px} .fav-modal .bm-gallery-mode .bm-nwrap .bm-stat{display:none} .fav-modal .bm-gallery-mode .bm-nwrap .bm-name{order:1;flex:1 1 auto;min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .fav-modal .bm-gallery-mode .bm-nwrap .bm-view,.fav-modal .bm-gallery-mode .bm-nwrap .bm-seen{position:absolute;bottom:51px;margin:0!important;padding:2px 7px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;line-height:1.3;white-space:nowrap;z-index:1;pointer-events:none} .fav-modal .bm-gallery-mode .bm-nwrap .bm-view{right:12px} .fav-modal .bm-gallery-mode .bm-nwrap .bm-seen{left:12px} .fav-modal .bm-gallery-mode .bm-nwrap .bm-view:empty,.fav-modal .bm-gallery-mode .bm-nwrap .bm-seen:empty{display:none} .fav-modal .bm-tools{margin-inline-end:-38px!important;justify-content:stretch!important} .fav-modal .bm-tools>.bm-wseg-host{flex:2 1 0!important;margin-inline-end:0!important} .fav-modal .bm-tools>.bm-hbtn{flex:1 1 0!important;width:auto!important;min-width:0!important} .fav-modal .bm-seg.bm-wseg{flex:1 1 auto;width:100%;max-width:none} .fav-modal .bm-seg.bm-wseg .bm-tbtn{flex:1 1 0;width:auto;padding:0!important;height:30px!important} .fav-modal .bm-seg.bm-wseg svg{width:21px;height:21px} .fav-modal .bm-tools .bm-prevbtn svg,.fav-modal .bm-tools .bm-colsbtn svg{width:18px;height:18px} .fav-modal .bm-tools .bm-hbtn.bm-gear{border:1px solid var(--th)!important} .fav-modal .bm-tbar{flex:none;display:block;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.07)} .fav-modal .bm-tbar .bm-tools{order:0!important;flex:none!important;margin:0!important;justify-content:stretch!important} .fav-modal .bm-tbar .bm-setp{margin-top:8px} .fav-modal.mode-hist .bm-tbar,.fav-modal.mode-cal .bm-tbar,.fav-modal.mode-mv .bm-tbar{display:none!important} .panel .hdr-btns .h-btn{width:30px;height:30px} .panel .hdr-btns .h-btn.lang{width:34px} .panel .hdr-btns .h-btn svg{width:22px!important;height:22px!important} .panel .hdr-btns{gap:3px;justify-content:flex-end;min-width:0} .panel .hdr-btns .h-btn{position:relative} .panel .hdr-btns{align-items:center} .panel .hdr-btns .hsep{flex:0 0 1px;width:1px;height:16px;margin:0 2px;background:rgba(255,255,255,.16);pointer-events:none} .panel .hdr-btns .stats+.hsep{margin-inline-start:0}.panel .hdr-btns{margin-right:2px} .panel .hdr-btns .stats.off+.hsep{display:none} .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del,.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin{top:5px;width:32px;height:32px;border-radius:8px} .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del{right:8px} .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin{right:46px} .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del svg,.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin svg{width:18px!important;height:18px!important} .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-nwrap{padding-inline-end:86px} :host([data-rtl]) .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del{right:auto;left:8px} :host([data-rtl]) .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin{right:auto;inset-inline-end:auto;left:46px} .fav-modal .bm-hdr .bm-tabs{display:flex!important;width:100%!important;max-width:100%!important} .fav-modal .bm-hdr .bm-tabs .bm-tbtn{flex:1 1 0!important;width:0!important;min-width:0!important;padding:0 4px!important;gap:4px!important;overflow:hidden;justify-content:center} .fav-modal .bm-hdr .bm-tabs .bm-tbtn .bm-tl{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .fav-modal .bm-hdr .bm-tabs .bm-tbtn svg{flex:none} .fav-modal .mv-pane>.bm-seg.mv-seg{margin-inline:14px!important;width:calc(100% - 28px)!important;display:flex!important;flex:0 0 auto!important;flex-direction:row!important;flex-wrap:nowrap!important;max-width:100%!important;min-height:30px;box-sizing:border-box;overflow:hidden} .fav-modal .mv-pane>.bm-seg.mv-seg .bm-tbtn{display:inline-flex!important;flex:1 1 0!important;width:0!important;min-width:0!important;padding:0 2px!important;gap:0!important;justify-content:center;align-items:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px!important} `; const CSS_POLISH = `@keyframes bcuIn{from{opacity:0;translate:0 8px}to{opacity:1;translate:0 0}} @keyframes bcuPop{from{opacity:0;scale:.96}to{opacity:1;scale:1}} @keyframes bcuRing{0%{box-shadow:0 0 0 0 var(--th)}100%{box-shadow:0 0 0 12px transparent}} :host ::selection{background:var(--th);color:#fff} :host .panel:not(.mini):not(.hidden){animation:bcuIn .28s cubic-bezier(.2,.8,.2,1)} :host .dlg-card,:host .cf-card,:host .pick-bar{animation:bcuPop .2s cubic-bezier(.2,.8,.2,1)} :host .bm-overlay .bm-modal{opacity:0;scale:.97;translate:0 10px;transition:opacity .2s ease,scale .26s cubic-bezier(.2,.8,.2,1),translate .26s cubic-bezier(.2,.8,.2,1)} :host .bm-overlay.open .bm-modal{opacity:1;scale:1;translate:0 0} :host .bm-overlay.bm-nofade .bm-modal{transition:none!important} :host .panel .btn span{display:flex;transition:transform .18s cubic-bezier(.2,.8,.2,1)} :host .panel .btn:hover span{transform:scale(1.1)} :host .panel .btn:active span{transform:scale(.94)} :host .panel .btn.on{box-shadow:inset 0 1px rgba(255,255,255,.12),0 2px 5px rgba(0,0,0,.4)} :host .panel .h-btn,:host .panel .z-btn,:host .panel .btn{-webkit-tap-highlight-color:transparent} :host .panel button:focus-visible,:host .bm-modal button:focus-visible,:host .dlg-row:focus-visible{outline:2px solid #fff;outline-offset:2px;box-shadow:0 0 0 4px var(--th)} :host :is(.vval,.s-val,.stats,.st-val,.bm-seen,.bm-view){font-variant-numeric:tabular-nums} :host .vslider{height:5px;transition:filter .15s} :host .vslider::-webkit-slider-thumb{width:15px;height:15px;transition:transform .15s,box-shadow .15s} :host .vslider::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:#fff;border:2px solid var(--th);cursor:pointer} :host .vslider:hover::-webkit-slider-thumb{transform:scale(1.15)} :host .vslider:active::-webkit-slider-thumb{transform:scale(1.25);box-shadow:0 0 0 7px color-mix(in srgb,var(--th) 28%,transparent)} :host .bm-sw{transition:background .22s ease} :host .bm-sw::after{transition:left .22s cubic-bezier(.3,1.3,.5,1),transform .15s;box-shadow:0 1px 3px rgba(0,0,0,.45)} :host .bm-sw:active::after{transform:scaleX(1.12)} :host .bm-modal .bm-close{transition:background .15s,color .15s} :host .bm-modal:not(.fav-modal) .bm-hdr{position:sticky;top:0;z-index:3;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)} :host .ghost:not([style*="display: none"]),:host .restore:not([style*="display: none"]){animation:bcuPop .25s cubic-bezier(.2,.8,.2,1)} :host .bm-empty{opacity:.85;line-height:1.5} :host .fav-modal .bm-list:not(.bm-gallery-mode) .bm-item{transition:background .2s,border-color .2s,transform .2s,box-shadow .2s} :host .fav-modal .bm-item:hover{transform:translateY(-1px);box-shadow:0 8px 20px -12px rgba(0,0,0,.8)} :host .fav-modal .bm-avatar{transition:transform .2s,filter .3s,border-color .3s} :host .fav-modal .bm-item:hover .bm-avatar{transform:scale(1.04)} :host *{scrollbar-width:thin} :host ::-webkit-scrollbar{width:8px;height:8px} :host ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16);border-radius:8px;border:2px solid transparent;background-clip:content-box} :host ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.3);background-clip:content-box;border:2px solid transparent} :host ::-webkit-scrollbar-track{background:transparent} @media (pointer:coarse){ :host .panel .btn{min-height:56px} :host .bm-modal .bm-close{min-width:40px;min-height:40px} :host .bm-modal .bm-btn,:host .bm-modal .bm-tbtn{min-height:40px} :host .dlg-row{min-height:48px} } @media (max-width:520px){ :host .bm-modal{max-width:100vw!important;width:100vw!important;border-radius:16px 16px 0 0!important;align-self:flex-end} :host .bm-overlay .bm-modal{translate:0 24px} :host .bm-overlay.open .bm-modal{translate:0 0} } @media (prefers-reduced-motion:reduce){ :host *,:host *::before,:host *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important} } `; const STAR_CSS = `.bcu-host{position:relative!important} .bcu-star{box-sizing:border-box;padding:0;line-height:0;position:absolute;left:6px;bottom:6px;z-index:6;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(0,0,0,.55);color:#fff;cursor:pointer;opacity:.92;transition:transform .15s,background .15s,color .15s;-webkit-tap-highlight-color:transparent;user-select:none;touch-action:manipulation} a.thm_a>.bcu-star{left:auto;right:6px} a.lst_link>.bcu-star{left:auto;right:6px;bottom:auto;top:50%;margin-top:-15px} a[data-model-list-link]>.bcu-star{left:auto;right:6px;bottom:auto;top:50%;margin-top:-15px} .bcu-star svg{display:block;flex:none;margin:0;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linejoin:round;pointer-events:none} .bcu-star:hover{transform:scale(1.12);background:rgba(0,0,0,.78)} .bcu-star.on{color:#ffc83d} .bcu-star.on svg{fill:currentColor} .bcu-ring{display:none;position:absolute;inset:0;z-index:5;border:2px solid #ffc83d;border-radius:inherit;box-sizing:border-box;pointer-events:none} .bcu-fav>.bcu-ring{display:block} `; class Helpers { static getModelSecretData() { try { for (const el of document.querySelectorAll('script[type="application/json"]')) { const t = el.textContent; if (!/chatHost|chatHeaderOptions|chatLocalData/.test(t)) continue; if (this._secretSrc === t) return this._secretData; this._secretSrc = t; this._secretData = null; return (this._secretData = JSON.parse(t)); } } catch (e) { dbg(e, 'Helpers.getModelSecretData'); } return null; } static createElement(tag, className = '') { const el = document.createElement(tag); if (className) el.className = className; return el; } static createButton(iconHTML, label, className = '') { const b = this.createElement('button', `btn ${className}`.trim()); b.innerHTML = `<span>${iconHTML}</span><sub>${label}</sub>`; return b; } static formatTime(s) { const p = n => String(n | 0).padStart(2, '0'); return `${p(s / 60)}:${p(s % 60)}`; } static getTimestamp() { const d = new Date(), p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`; } static download(blob, filename) { const url = URL.createObjectURL(blob), a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = String(filename).replace(/[\\/:*?"<>|]+/g, '_'); document.body.appendChild(a); a.click(); setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 60000); } static bindLongPress(el, onClick, onHold, ms = 400) { let timer = 0, fired = false, active = false, sx = 0, sy = 0; const clear = () => { clearTimeout(timer); timer = 0; }; const reset = () => { active = false; fired = false; clear(); }; const safe = (fn, e, what) => { try { fn(e); } catch (err) { console.error('[BCU] ' + what, err); } }; el.oncontextmenu = e => e.preventDefault(); el.onpointerdown = e => { if (!isPrimaryPress(e)) return; active = true; fired = false; sx = e.clientX; sy = e.clientY; clear(); timer = setTimeout(() => { timer = 0; if (!active) return; fired = true; safe(onHold, e, 'hold'); }, ms); }; el.onpointermove = e => { if (active && !fired && Math.hypot(e.clientX - sx, e.clientY - sy) > 14) reset(); }; el.onpointerup = e => { if (!active) return; const wasHold = fired; reset(); if (!wasHold) safe(onClick, e, 'click'); }; el.onpointercancel = reset; el.onpointerleave = e => { if (e.pointerType === 'mouse') reset(); }; } static snap(v, name, done) { try { const c = document.createElement('canvas'); c.width = v.videoWidth || 1280; c.height = v.videoHeight || 720; c.getContext('2d').drawImage(v, 0, 0); c.toBlob(b => { if (!b) return done(false); Helpers.download(b, `${name || 'Cam'}_${Helpers.getTimestamp()}.jpg`); done(true); }, 'image/jpeg', 0.95); } catch { done(false); } } static bindHold(el, { ms = 450, dist = 10, ok, onHold, always = false }) { let t = 0, done = false, armed = false, x = 0, y = 0, type = 'mouse'; const clear = () => { clearTimeout(t); t = 0; }; el.addEventListener('pointerdown', e => { clear(); done = false; armed = false; if (e.pointerType === 'mouse' && e.button !== 0) return; if (ok && !ok(e)) return; armed = true; type = e.pointerType; x = e.clientX; y = e.clientY; t = setTimeout(() => { t = 0; done = true; onHold(e); }, ms); }); el.addEventListener('pointermove', e => { if (t && Math.hypot(e.clientX - x, e.clientY - y) > dist) clear(); }); ['pointerup', 'pointercancel', 'pointerleave'].forEach(n => el.addEventListener(n, clear)); el.addEventListener('click', e => { if (done) { done = false; e.preventDefault(); e.stopImmediatePropagation(); } }, true); el.addEventListener('contextmenu', e => { if (always || (armed && (type !== 'mouse' || done))) e.preventDefault(); }); } static trackDrag(e, { onMove, onEnd, minDist = 2, capEl }) { const el = capEl || e.target, sX = e.clientX, sY = e.clientY; try { el.setPointerCapture(e.pointerId); } catch (e) { dbg(e, 'Helpers.trackDrag'); } let moved = false; const move = ev => { const dx = ev.clientX - sX, dy = ev.clientY - sY; if (!moved && Math.hypot(dx, dy) > minDist) moved = true; if (moved) onMove(dx, dy, ev); }; const end = ev => { try { el.releasePointerCapture(ev.pointerId); } catch (e) { dbg(e, 'trackDrag.end'); } window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', end); window.removeEventListener('pointercancel', end); onEnd?.(moved, ev); }; window.addEventListener('pointermove', move, { passive: false }); window.addEventListener('pointerup', end); window.addEventListener('pointercancel', end); } static copy(text, ok, err) { text = String(text); const legacy = () => { try { const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none'; (document.body || document.documentElement).appendChild(ta); ta.select(); ta.setSelectionRange(0, text.length); const done = document.execCommand('copy'); ta.remove(); return done; } catch { return false; } }; const fail = e => { if (legacy()) ok?.(); else err?.(e); }; try { if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(() => ok?.(), fail); else fail(); } catch (e) { fail(e); } } static flashBtn(btn, ok) { const orig = btn.innerHTML; btn.innerHTML = ok ? icons.ok : '<span style="color:#e74c3c">ERR</span>'; setTimeout(() => { if (btn.isConnected) btn.innerHTML = orig; }, 1000); } } const mk = (tag, className) => Helpers.createElement(tag, className); const bindGear = (btn, panel) => { btn.onclick = () => { const o = panel.classList.toggle('open'); btn.classList.toggle('on', o); }; }; const mkSetRow = (label, ctl) => { const r = mk('div', 'bm-setrow'); const t = mk('span'); t.textContent = label; r.append(t, ctl); return r; }; const isPrimaryPress = e => e.button === 0 || e.pointerType !== 'mouse'; const swallow = e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }; const bindEscape = close => { const onKey = e => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); } }; document.addEventListener('keydown', onKey, true); return () => document.removeEventListener('keydown', onKey, true); }; const onRemoteChange = (key, handler) => { if (typeof GM_addValueChangeListener !== 'function') return; try { GM_addValueChangeListener(Store.key(key), (n, o, v, remote) => { if (remote) handler(v); }); } catch (e) { dbg(e, 'onRemoteChange'); } }; const injectShadowCss = (shadow, key, css) => { if (shadow[key]) return; shadow[key] = true; const st = document.createElement('style'); st.textContent = css; shadow.appendChild(st); }; const DLG_CSS = ` .bm-grp-btn{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;text-align:start;cursor:pointer;font-family:inherit} .bm-grp-arr{opacity:.6;font-size:12px} .dlg-ov{pointer-events:auto;position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);animation:dlgIn .15s ease} @keyframes dlgIn{from{opacity:0}to{opacity:1}} .dlg-card{width:min(340px,100%);max-height:80%;display:flex;flex-direction:column;background:#1c1c20;border:1px solid rgba(255,255,255,.1);border-radius:14px;box-shadow:0 18px 50px rgba(0,0,0,.6);overflow:hidden;color:#eee;font-size:15px} .dlg-list{overflow-y:auto;padding:6px} .dlg-row{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;padding:13px 12px;border:0;border-radius:8px;background:transparent;color:#eee;font:inherit;text-align:start;cursor:pointer} .dlg-row:active{background:rgba(255,255,255,.08)} .dlg-row.on{background:rgba(255,255,255,.07)} .dlg-dot{flex:none;width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.35)} .dlg-row.on .dlg-dot{border-color:var(--th,#4aa3ff);background:radial-gradient(circle,var(--th,#4aa3ff) 0 45%,transparent 50%)} .dlg-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .dlg-new{display:flex;gap:8px;padding:10px;border-top:1px solid rgba(255,255,255,.08)} .dlg-in{flex:1;min-width:0;height:40px;padding:0 12px;border-radius:8px;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.12);color:#f0f0f0;-webkit-text-fill-color:#f0f0f0;font:inherit;outline:none} .dlg-in:focus{border-color:var(--th,#4aa3ff)} .dlg-ok{flex:none;height:40px;padding:0 14px;border:0;border-radius:8px;background:var(--th,#4aa3ff);color:#fff;font:inherit;font-weight:600;cursor:pointer} `; const openGroupDialog = (ui, { current, groups, onPick }) => { const shadow = ui.shadow; injectShadowCss(shadow, '_dlgCss', DLG_CSS); const ov = mk('div', 'dlg-ov'); const card = mk('div', 'dlg-card'); const list = mk('div', 'dlg-list'); const unbindEsc = bindEscape(() => close()); const close = () => { ov.remove(); unbindEsc(); }; const pick = v => { close(); if (v !== current) onPick(v); }; const row = (label, value) => { const r = mk('button', 'dlg-row' + (value === current ? ' on' : '')); r.type = 'button'; const t = mk('span', 'dlg-txt'); t.innerHTML = `${icons.folder}<span>${escHTML(label)}</span>`; r.append(t, mk('span', 'dlg-dot')); r.onclick = e => { e.stopPropagation(); pick(value); }; return r; }; list.append(row(L().grp_none, ''), ...groups.map(g => row(g, g))); const nw = mk('div', 'dlg-new'); const inp = mk('input', 'dlg-in'); inp.type = 'text'; inp.maxLength = 40; inp.placeholder = L().grp_new.replace(/^\+\s*/, '').replace(/…$/, '') + '…'; inp.autocomplete = 'off'; inp.spellcheck = false; const ok = mk('button', 'dlg-ok'); ok.type = 'button'; ok.textContent = '+'; const submit = () => { const v = inp.value.trim().slice(0, 40); if (v) pick(v); else inp.focus(); }; ok.onclick = e => { e.stopPropagation(); submit(); }; inp.onkeydown = e => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); submit(); } }; inp.onkeyup = e => e.stopPropagation(); nw.append(inp, ok); card.append(list, nw); card.onclick = e => e.stopPropagation(); ov.onclick = close; ov.append(card); shadow.appendChild(ov); }; const FRAME_CSS = `.fr-ov{pointer-events:auto;position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.8);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}.fr-card{max-width:100%;max-height:100%;display:flex;align-items:center;justify-content:center}.fr-img{max-width:100%;max-height:90vh;border-radius:3px;box-shadow:0 18px 50px rgba(0,0,0,.6);display:block}.fr-spd{position:absolute;left:50%;bottom:calc(18px + env(safe-area-inset-bottom,0px));transform:translateX(-50%);min-width:64px;padding:10px 18px;font:bold 15px/1 system-ui,sans-serif;color:#eee;background:rgba(28,28,32,.92);border:1px solid rgba(255,255,255,.18);border-radius:12px;cursor:pointer}.fr-msg{color:#eee;font-size:15px;text-align:center;padding:18px 22px;border-radius:12px;background:#1c1c20;border:1px solid rgba(255,255,255,.1)}`; const loadFrame = async (url, gmFirst) => { if (gmFirst) { const g = await gmImgData(url, true).catch(() => ''); if (g) return g; } const ok = await new Promise(res => { const t = new Image(); t.onload = () => res(true); t.onerror = () => res(false); t.src = url; }); return ok ? url : gmImgData(url, true).catch(() => ''); }; const frameShowAvatar = async (b, fv) => { const list = [toHttps(b.avatar), toHttps(histLoad().m[bmKey(b.url)]?.a)].filter(Boolean); for (const u of list) { const d = await loadFrame(u); if (fv.closed) return; if (d) { fv.img.style.cssText = 'width:min(100%,560px);height:auto;object-fit:contain'; fv.img.src = d; fv.card.replaceChildren(fv.img); return; } } fv.msg.textContent = L().fr_none; fv.card.replaceChildren(fv.msg); }; const PREV_SPD = [1000, 2000, 500, 200, 0]; const prevSpd = () => { const v = Store.get('bc_prev_spd', 1000); return PREV_SPD.includes(v) ? v : 1000; }; const prevSpdNext = () => PREV_SPD[(PREV_SPD.indexOf(prevSpd()) + 1) % PREV_SPD.length]; const prevSpdTitle = () => prevSpd() ? L().st_prev_spd : `${L().st_prev_spd}: ${L().st_prev_off}`; const prevSpdLabel = v => v ? `${(v / 1000).toFixed(1)}s` : '0'; const PREV_SPD_SEG = [200, 500, 1000, 2000]; const frameLive = (b, fv, frame, onFirstFail) => { const tick = async () => { if (fv.closed || fv.busy) return; fv.busy = true; clearTimeout(fv.timer); const url = frameBust(frame); const d = await loadFrame(url, !!b._lastData); fv.busy = false; if (fv.closed) return; if (d) { fv.img.src = d; if (d === url) { b._lastImg = url; b._lastData = ''; } else b._lastData = d; } else if (!fv.img.isConnected) { return onFirstFail(); } const sp = prevSpd(); if (sp) fv.timer = setTimeout(tick, sp); }; fv.tick = tick; tick(); }; const frameViaProfile = async (b, fv) => { const fail = () => { if (!fv.closed) frameShowAvatar(b, fv); }; const h = await requestProfile(b); if (fv.closed) return; const info = h ? parseProfile(h, urlSite(b.url), modelSlug(b.url)) : null; const fr = info && info.status ? info.frame : ''; if (!fr) return fail(); frameLive(b, fv, fr, fail); }; const openFrameView = (ui, b) => { const shadow = ui.shadow; injectShadowCss(shadow, '_frCss', FRAME_CSS); const ov = mk('div', 'fr-ov'); const card = mk('div', 'fr-card'); const msg = mk('div', 'fr-msg'); msg.textContent = '…'; const img = mk('img', 'fr-img'); const fv = { closed: false, timer: 0, img, card, msg }; const spdBtn = mk('button', 'fr-spd'); const paintSpd = () => { spdBtn.textContent = prevSpdLabel(prevSpd()); spdBtn.title = prevSpdTitle(); }; paintSpd(); spdBtn.onclick = e => { e.stopPropagation(); Store.set('bc_prev_spd', prevSpdNext()); paintSpd(); clearTimeout(fv.timer); if (prevSpd() && fv.tick) fv.tick(); }; img.onload = () => { if (!fv.closed && !img.isConnected) card.replaceChildren(img); }; const unbindEsc = bindEscape(() => close()); const close = () => { fv.closed = true; clearTimeout(fv.timer); ov.remove(); unbindEsc(); }; ov.onclick = close; card.append(msg); ov.append(card, spdBtn); shadow.appendChild(ov); if (b.frame && (b.status ?? 0) !== 0) { const cached = b._lastData || b._lastImg; if (cached) img.src = cached; frameLive(b, fv, b.frame, () => frameViaProfile(b, fv)); } else frameViaProfile(b, fv); }; const BM_HLS = new Map(); const copyHls = (u, cb) => u ? Helpers.copy(u, () => cb(true), () => cb(false)) : cb(false); const shareHls = (u, title, cb) => (u && navigator.share) ? navigator.share({ title, url: u }).catch(() => {}) : cb(false); const largestVideo = () => { let best = null, max = 0; for (const cur of document.querySelectorAll('video')) { const area = (cur.videoWidth * cur.videoHeight) || (cur.offsetWidth * cur.offsetHeight) || 0; if (area > max) { max = area; best = cur; } } return best; }; const selectorVideo = () => currentSite.videoSelectors.map(s => safeQuery(s)).find(el => el?.tagName === 'VIDEO'); const safeClosest = (el, sel) => { try { return el.closest(sel); } catch { return null; } }; const safeQuery = sel => { try { return document.querySelector(sel); } catch { return null; } }; const isQualityShown = q => q.style.display === 'block' || (!q.classList.contains('__hidden') && q.offsetWidth > 0); const jq = () => window.unsafeWindow?.$ || window.$ || window.jQuery; const fsActive = () => document.fullscreenElement || document.webkitFullscreenElement; const fsRequest = el => (el.requestFullscreen || el.webkitRequestFullscreen)?.call(el); const fsExit = () => (document.exitFullscreen || document.webkitExitFullscreen)?.call(document); const containerOf = v => currentSite.containerSelectors.map(s => safeClosest(v, s)).find(Boolean) || v.parentElement; const isTyping = e => { try { const t = e.composedPath()[0]; return !!(t?.matches?.('input, textarea, select') || t?.isContentEditable); } catch { return false; } }; const volIcon = (vol, muted) => muted ? icons.mute : vol < 0.4 ? icons.volLow : vol < 0.75 ? icons.volMed : icons.vol; const VOL_STEP = { ArrowUp: 0.1, ArrowDown: -0.1 }; const VJS_UI = '.vjs-control-bar,.vjs-control,.vjs-button,.vjs-menu,.vjs-menu-item,.vjs-menu-content,[role="menu"],[role="menuitem"],[role="button"],[role="slider"],button,a'; class VideoController { constructor(uiController) { this.ui = uiController; this.cachedVideo = null; this.lastVolume = 1; this.isTheater = false; this.theaterWrap = null; this._anc = null; this.bindEvents(); this.bindGlobalTap(); this._findTimer = setInterval(() => { if (!realHidden()) this.findVideo(); }, 2000); onPageEvent('visibilitychange', () => { if (!realHidden()) this.findVideo(); }); } bindEvents() { const actions = { KeyM: () => this.toggleMute(), KeyF: () => this.toggleFullscreen(), KeyP: () => this.cachedVideo && this.togglePiP(), KeyT: () => this.toggleTheater(), KeyS: () => this.takeScreenshot(), KeyH: () => this.ui.toggleHide(!AppState.isHidden) }; const hasMod = e => e.ctrlKey || e.altKey || e.metaKey; window.addEventListener('keydown', e => { if (isTyping(e) || hasMod(e)) return; const act = actions[e.code]; if (act) { e.preventDefault(); if (!e.repeat) act(); return; } const step = VOL_STEP[e.code]; if (e.shiftKey && step && this.cachedVideo) { e.preventDefault(); this.setVolume(this.cachedVideo.volume + step); } }); } findVideo() { const cv0 = this.cachedVideo; if (cv0?.isConnected) { if (cv0.videoWidth || cv0.offsetWidth) return; const alt = largestVideo(); if (!alt || alt === cv0 || !(alt.videoWidth || alt.offsetWidth)) return; } if (this.cachedVideo) { if (this.isTheater) this.toggleTheater(); this.cachedVideo = this.theaterWrap = null; } const v = largestVideo() || selectorVideo() || null; if (v !== this.cachedVideo) { this.cachedVideo = v; if (v) this.attachInteractions(v); } } attachInteractions(v) { if (v._bcBound) return; v._bcBound = true; this._vcEvts = this._vcEvts || (e => { const cv = this.cachedVideo; if (e?.type === 'volumechange' && AppState.gmute && cv && (!cv.muted || cv.volume > 0)) { cv.muted = true; cv.volume = 0; } this.syncUI(); }); ['volumechange', 'pause', 'play'].forEach(ev => v.addEventListener(ev, this._vcEvts, { passive: true })); v.addEventListener('enterpictureinpicture', () => this.pipPoster(v, true)); v.addEventListener('leavepictureinpicture', () => this.pipPoster(v, false)); if (AppState.gmute) { v.muted = true; v.volume = 0; } if (!/(^|\.)chaturbate\.com$/.test(location.hostname)) { v.style.setProperty('z-index', '9999', 'important'); v.parentElement?.style.setProperty('z-index', '9999', 'important'); } const target = containerOf(v); if (target && !target._bcTouchBound) { target._bcTouchBound = true; this.bindWheel(target); this.bindDoubleClick(target); } this.syncUI(); } bindWheel(target) { let pending = false; target.addEventListener('wheel', e => { if (safeClosest(e.target, '.vjs-control-bar, .vjs-menu, [role="slider"], [role="menu"]')) return; if (!this.isTheater && !document.fullscreenElement && !document.webkitFullscreenElement && !e.shiftKey) return; e.preventDefault(); if (pending) return; pending = true; requestAnimationFrame(() => { pending = false; const cv = this.cachedVideo; if (cv) this.setVolume(cv.volume + (e.deltaY < 0 ? 0.05 : -0.05)); }); }, { passive: false }); } bindDoubleClick(target) { let clicks = 0, timer = null, sX = 0, sY = 0; target.addEventListener('pointerdown', e => { sX = e.clientX; sY = e.clientY; }, { passive: true, capture: true }); target.addEventListener('click', e => { if (Date.now() - (this._tapAt || 0) < 600) { swallow(e); return; } if (safeClosest(e.target, VJS_UI)) return; if (Math.abs(e.clientX - sX) > 10 || Math.abs(e.clientY - sY) > 10) return; if (++clicks === 1) { timer = setTimeout(() => { clicks = 0; }, 250); return; } swallow(e); clearTimeout(timer); clicks = 0; this.toggleFullscreen(); }, true); } bindGlobalTap() { let dX = 0, dY = 0, dT = 0, lastT = 0, lastX = 0, lastY = 0; const inUi = e => { try { return e.composedPath().some(n => n && (n.id === 'bc-unchained-host' || n.id === 'bc-cb-qual')); } catch { return false; } }; const box = () => { const v = this.cachedVideo; return v && v.isConnected ? (containerOf(v) || v) : null; }; const smallControl = (el, r) => { const c = safeClosest(el, VJS_UI); if (!c) return false; const cr = c.getBoundingClientRect(); return cr.width * cr.height < r.width * r.height * 0.5; }; window.addEventListener('pointerdown', e => { if (e.pointerType === 'mouse') return; dX = e.clientX; dY = e.clientY; dT = Date.now(); }, { capture: true, passive: true }); window.addEventListener('pointerup', e => { if (e.pointerType === 'mouse' || !e.isPrimary || inUi(e)) return; const now = Date.now(); if (now - dT > 350 || Math.abs(e.clientX - dX) > 10 || Math.abs(e.clientY - dY) > 10) { lastT = 0; return; } const b = box(); if (!b) { lastT = 0; return; } const r = b.getBoundingClientRect(); if (r.width < 1 || e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom || smallControl(e.target, r)) { lastT = 0; return; } if (lastT && now - lastT < 350 && Math.abs(e.clientX - lastX) < 40 && Math.abs(e.clientY - lastY) < 40) { lastT = 0; this._tapAt = now; this.toggleFullscreen(); return; } lastT = now; lastX = e.clientX; lastY = e.clientY; }, { capture: true, passive: true }); } setVolume(val) { const v = this.cachedVideo; if (!v) return; val = clamp(val, 0, 1); if (val > 0 && AppState.gmute) setGMute(false); v.volume = val; if (val > 0) { v.muted = false; this.lastVolume = val; } this.syncUI(); } clickSiteUnmute() { try { const sb = document.querySelector('.stream_muted_button'); if (sb && sb.offsetParent !== null) sb.click(); } catch (e) { dbg(e, 'VideoController.clickSiteUnmute'); } } toggleMute() { const v = this.cachedVideo; if (!v) return; if (v.muted || v.volume === 0) this.unmuteVideo(v); else this.muteVideo(v); this.syncUI(); } unmuteVideo(v) { if (AppState.gmute) setGMute(false); this.clickSiteUnmute(); v.muted = false; v.volume = this.lastVolume || 1; } muteVideo(v) { v.muted = true; this.lastVolume = v.volume || 1; } pipPoster(v, on) { try { if (on) { if (v.hasAttribute('poster') && !v._bcPoster) return; if (!v.videoWidth || !v.videoHeight) return; const w = 320, h = Math.max(1, Math.round(w * v.videoHeight / v.videoWidth)); const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(v, 0, 0, w, h); const url = c.toDataURL('image/jpeg', 0.7); if (url && url.length > 100) { v._bcPoster = true; v.setAttribute('poster', url); } } else if (v._bcPoster) { v._bcPoster = false; v.removeAttribute('poster'); } } catch (e) { dbg(e, 'VideoController.pipPoster'); } } async togglePiP() { const v = this.cachedVideo; if (!v) return; try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else if (document.pictureInPictureEnabled && v.requestPictureInPicture) { v.removeAttribute('disablePictureInPicture'); this.pipPoster(v, true); await v.requestPictureInPicture(); } else this.ui.flash(this.ui.elements.bPip, 'ERR'); this.ui?.elements?.bPip?.classList.toggle('on', !!document.pictureInPictureElement); } catch { this.ui.flash(this.ui.elements.bPip, 'ERR'); } } toggleFullscreen() { const t = currentSite.containerSelectors.map(s => safeQuery(s)).find(Boolean) || (this.cachedVideo && containerOf(this.cachedVideo)) || this.cachedVideo; if (!t) return; try { fsActive() ? fsExit() : fsRequest(t); } catch (e) { dbg(e, 'VideoController.toggleFullscreen'); } } enterTheater(v) { this.theaterWrap = containerOf(v); this.theaterWrap?.classList.add('bc-thtr-wrap'); v.classList.add('bc-thtr-video'); this._anc = []; for (let e = v.parentElement; e && !isRootEl(e); e = e.parentElement) { e.classList.add('bc-thtr-anc'); this._anc.push(e); } window.scrollTo(0, 0); } leaveTheater(v) { this.theaterWrap?.classList.remove('bc-thtr-wrap'); v?.classList.remove('bc-thtr-video'); this._anc?.forEach(e => e.classList.remove('bc-thtr-anc')); this._anc = null; this.theaterWrap = null; } cbTheaterControls(on) { const id = 'bc-cb-controls-theater'; document.getElementById(id)?.remove(); if (!on || !/(^|\.)chaturbate\.com$/.test(location.hostname)) return; const st = document.createElement('style'); st.id = id; st.textContent = '#mobileVideoControls,.MobileVideoControls{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;height:100dvh!important;z-index:2147483645!important}'; (document.head || document.documentElement).appendChild(st); } toggleTheater() { const v = this.cachedVideo, on = !this.isTheater; if (on && !v) return; this.isTheater = on; this.ui?.elements?.bThtr?.classList.toggle('on', on); document.body.classList.toggle('bc-theater-mode', on); document.documentElement.classList.toggle('bc-theater-html', on); on ? this.enterTheater(v) : this.leaveTheater(v); this.cbTheaterControls(on); window.dispatchEvent(new Event('resize')); } takeScreenshot() { const v = this.cachedVideo; if (!v) return; const b = this.ui.elements.bShot; Helpers.snap(v, currentSite.getName(), ok => this.ui.flash(b, ok ? 'OK' : 'ERR')); } paintMuteBtn(btn, muted, gMute) { const sub = btn.querySelector('sub'); if (sub) sub.textContent = gMute ? 'LOCK' : L().sound; btn.firstChild.innerHTML = muted ? icons.mute : icons.vol; btn.classList.toggle('on', muted); btn.style.boxShadow = gMute ? `inset 0 0 0 2px #e74c3c` : ''; } syncUI() { const v = this.cachedVideo, ui = this.ui, el = ui.elements; if (!v || !el.bMute) return; const vol = v.muted ? 0 : v.volume; const muted = v.muted || vol === 0; const gMute = AppState.gmute; const key = `${vol}|${muted}|${gMute}`; if (ui._volKey === key) return; ui._volKey = key; this.paintMuteBtn(el.bMute, muted, gMute); el.vSlider.value = String(vol); el.vSlider.style.setProperty('--p', `${Math.round(vol * 100)}%`); el.vIco.innerHTML = volIcon(vol, muted); el.vVal.textContent = `${Math.round(vol * 100)}%`; } } const WEBM_HEAD = 8192; const vintReader = head => (p, raw) => { const f = head[p]; if (!f) return null; const len = Math.clz32(f) - 23; if (len > 8 || p + len > head.length) return null; const mask = 0xFF >> len; let v = raw ? f : (f & mask), ones = (f & mask) === mask; for (let i = 1; i < len; i++) { v = v * 256 + head[p + i]; if (head[p + i] !== 0xFF) ones = false; } return { v, len, unknown: ones }; }; const webmSegmentStart = rd => { const ebml = rd(0, true); if (!ebml || ebml.v !== 0x1A45DFA3) return -1; const es = rd(ebml.len, false); if (!es) return -1; const p = ebml.len + es.len + es.v; const seg = rd(p, true); if (!seg || seg.v !== 0x18538067) return -1; const ss = rd(p + seg.len, false); return ss?.unknown ? p + seg.len + ss.len : -1; }; const webmInfoScale = (rd, head, ds, de) => { let q = ds, scale = 1e6; while (q < de) { const cid = rd(q, true); const csz = cid && rd(q + cid.len, false); if (!csz || cid.v === 0x4489) return null; const cs = q + cid.len + csz.len; if (cid.v === 0x2AD7B1) { scale = 0; for (let i = 0; i < csz.v; i++) scale = scale * 256 + head[cs + i]; } q = cs + csz.v; } return scale; }; const webmWithDuration = (blob, head, p, idLen, ds, de, durationMs, scale) => { const dur = new Uint8Array(11); dur.set([0x44, 0x89, 0x88]); new DataView(dur.buffer).setFloat64(3, durationMs * 1e6 / scale); const size8 = new Uint8Array(8); size8[0] = 0x01; new DataView(size8.buffer).setUint32(4, (de - ds) + dur.length); return new Blob([ head.subarray(0, p), head.subarray(p, p + idLen), size8, head.subarray(ds, de), dur, head.subarray(de), blob.slice(WEBM_HEAD) ], { type: blob.type }); }; const webmElement = (rd, p) => { const id = rd(p, true); const sz = id && rd(p + id.len, false); if (!sz || sz.unknown) return null; const ds = p + id.len + sz.len; return { id, ds, de: ds + sz.v }; }; const fixWebmDuration = async (blob, durationMs) => { const head = new Uint8Array(await blob.slice(0, WEBM_HEAD).arrayBuffer()); const rd = vintReader(head); let p = webmSegmentStart(rd); while (p >= 0 && p < head.length) { const el = webmElement(rd, p); if (!el) break; if (el.id.v === 0x114D9B74) return blob; if (el.id.v === 0x1549A966) { const scale = el.de > head.length ? null : webmInfoScale(rd, head, el.ds, el.de); return scale === null ? blob : webmWithDuration(blob, head, p, el.id.len, el.ds, el.de, durationMs, scale); } p = el.de; } return blob; }; const u8cat = parts => { let n = 0; for (const p of parts) n += p.length; const o = new Uint8Array(n); let q = 0; for (const p of parts) { o.set(p, q); q += p.length; } return o; }; const u8ascii = s => Uint8Array.from(s, c => c.charCodeAt(0)); const MediaFix = (() => { const rd32 = (b, p) => ((b[p] << 24) | (b[p + 1] << 16) | (b[p + 2] << 8) | b[p + 3]) >>> 0; const rd64 = (b, p) => rd32(b, p) * 4294967296 + rd32(b, p + 4); const tag = (b, p) => String.fromCharCode(b[p], b[p + 1], b[p + 2], b[p + 3]); const w32 = n => { const o = new Uint8Array(4); new DataView(o.buffer).setUint32(0, n >>> 0); return o; }; const w64 = n => { const o = new Uint8Array(8), dv = new DataView(o.buffer); dv.setUint32(0, Math.floor(n / 4294967296)); dv.setUint32(4, n >>> 0); return o; }; const box = (type, ...parts) => { const body = u8cat(parts), o = new Uint8Array(8 + body.length); new DataView(o.buffer).setUint32(0, o.length); o.set(u8ascii(type), 4); o.set(body, 8); return o; }; const fullBox = (type, ver, flags, ...parts) => box(type, Uint8Array.of(ver, (flags >> 16) & 255, (flags >> 8) & 255, flags & 255), ...parts); const words = (list, per) => { const o = new Uint8Array(list.length * per * 4), dv = new DataView(o.buffer); let q = 0; for (const e of list) for (let i = 0; i < per; i++, q += 4) dv.setUint32(q, (per === 1 ? e : e[i]) >>> 0); return o; }; const rle = arr => { const out = []; for (const v of arr) { const l = out[out.length - 1]; if (l && l[0] === v) l[1]++; else out.push([v, 1]); } return out; }; const kids = (b, s, e) => { const out = []; let p = s; while (p + 8 <= e) { let sz = rd32(b, p), hdr = 8; if (sz === 1) { sz = rd64(b, p + 8); hdr = 16; } else if (sz === 0) sz = e - p; if (sz < hdr || p + sz > e) break; out.push({ t: tag(b, p + 4), s: p, e: p + sz, d: p + hdr }); p += sz; } return out; }; const kid = (list, t) => list.find(x => x.t === t); const setDur = (copy, off, v1, value) => { const dv = new DataView(copy.buffer); if (v1) { dv.setUint32(off, Math.floor(value / 4294967296)); dv.setUint32(off + 4, value >>> 0); } else dv.setUint32(off, value >>> 0); }; const req = (msg, ...xs) => { if (xs.some(x => !x)) throw new Error(msg); }; const slice = (moov, c) => moov.slice(c.s, c.e); const rest = (moov, list, skip) => list.filter(c => !skip.includes(c.t)).map(c => slice(moov, c)); const patchDur = (moov, c, off, v1, val) => { const cp = slice(moov, c); setDur(cp, off - c.s, v1, val); return cp; }; const rleBox = (type, ver, arr) => { const r = rle(arr); return fullBox(type, ver, 0, words([r.length], 1), words(r.map(([v, c]) => [c, v]), 2)); }; const boxHead = (b, p, avail) => { let sz = rd32(b, p), hdr = 8; if (sz === 1) { sz = rd64(b, p + 8); hdr = 16; } else if (sz === 0) sz = avail; return sz < hdr ? null : { sz, hdr }; }; const scanTop = async (blob, size) => { const top = []; for (let p = 0; p + 8 <= size;) { const h = new Uint8Array(await blob.slice(p, Math.min(size, p + 16)).arrayBuffer()); const bh = boxHead(h, 0, size - p); if (!bh || p + bh.sz > size) break; top.push({ t: tag(h, 4), s: p, e: p + bh.sz, d: p + bh.hdr }); p += bh.sz; } return top; }; const readTrex = (moov, mk) => { const trex = {}; const mvex = kid(mk, 'mvex'); if (!mvex) return trex; for (const c of kids(moov, mvex.d, mvex.e).filter(x => x.t === 'trex')) { trex[rd32(moov, c.d + 4)] = { dur: rd32(moov, c.d + 12), size: rd32(moov, c.d + 16), flags: rd32(moov, c.d + 20) }; } return trex; }; const parseTrak = (moov, trak, trex) => { const tk = kids(moov, trak.d, trak.e); const tkhd = kid(tk, 'tkhd'), mdia = kid(tk, 'mdia'); req('bad trak', tkhd, mdia); const tv1 = moov[tkhd.d] === 1, idOff = tkhd.d + 4 + (tv1 ? 16 : 8); const id = rd32(moov, idOff); const mk2 = kids(moov, mdia.d, mdia.e); const mdhd = kid(mk2, 'mdhd'), minf = kid(mk2, 'minf'); req('bad mdia', mdhd, minf); const dv1 = moov[mdhd.d] === 1, tsOff = mdhd.d + 4 + (dv1 ? 16 : 8); const mi = kids(moov, minf.d, minf.e), stbl = kid(mi, 'stbl'); const stsd = stbl && kid(kids(moov, stbl.d, stbl.e), 'stsd'); req('no stsd', stbl, stsd); return { id, trak, tk, tkhd, tv1, durOffT: idOff + 8, mdia, mk2, mdhd, dv1, ts: rd32(moov, tsOff), durOffM: tsOff + 4, minf, mi, stbl, stsd: moov.slice(stsd.s, stsd.e), dur: [], sz: [], sync: [], cto: [], chunks: [], dts: 0, start: 0, negCto: false, trex: trex[id] || { dur: 0, size: 0, flags: 0 } }; }; const parseTracks = (moov, mk, trex) => { const order = mk.filter(x => x.t === 'trak').map(trak => parseTrak(moov, trak, trex)); if (!order.length) throw new Error('no tracks'); const tracks = {}; for (const tr of order) tracks[tr.id] = tr; return { tracks, order }; }; const TFHD_DEFAULTS = [[0x08, 'dur'], [0x10, 'size'], [0x20, 'flags']]; const readTfhd = (b, tfhd, trex) => { const fl = rd32(b, tfhd.d) & 0xffffff; const th = { fl, baseOff: null, dur: trex.dur, size: trex.size, flags: trex.flags }; let o = tfhd.d + 8; if (fl & 1) { th.baseOff = rd64(b, o); o += 8; } if (fl & 2) o += 4; for (const [bit, key] of TFHD_DEFAULTS) if (fl & bit) { th[key] = rd32(b, o); o += 4; } return th; }; const fragBase = (th, first, moofStart, lastEnd) => { if (th.baseOff !== null) return th.baseOff; return ((th.fl & 0x20000) || first) ? moofStart : lastEnd; }; const applyTfdt = (b, tf, tr) => { const tfdt = kid(tf, 'tfdt'); if (!tfdt) return; const t = b[tfdt.d] === 1 ? rd64(b, tfdt.d + 4) : rd32(b, tfdt.d + 4); if (!tr.dur.length) { tr.start = t; tr.dts = t; return; } if (t === tr.dts) return; const nl = tr.dur[tr.dur.length - 1] + (t - tr.dts); if (nl > 0) tr.dur[tr.dur.length - 1] = nl; tr.dts = t; }; const TRUN_FIELDS = [[0x100, 'd'], [0x200, 's'], [0x400, 'f']]; const readSample = (b, q, rf, rv, smp) => { for (const [bit, key] of TRUN_FIELDS) if (rf & bit) { smp[key] = rd32(b, q); q += 4; } if (rf & 0x800) { smp.c = rv ? (rd32(b, q) | 0) : rd32(b, q); q += 4; } return q; }; const readTrun = (b, run, base, nextPos, th, tr) => { const rf = rd32(b, run.d) & 0xffffff, rv = b[run.d], n = rd32(b, run.d + 4); let q = run.d + 8, pos = nextPos, firstFlags = null; if (rf & 1) { pos = base + (rd32(b, q) | 0); q += 4; } if (rf & 4) { firstFlags = rd32(b, q); q += 4; } let total = 0; for (let i = 0; i < n; i++) { const smp = { d: th.dur, s: th.size, f: (i === 0 && firstFlags !== null) ? firstFlags : th.flags, c: 0 }; q = readSample(b, q, rf, rv, smp); if (smp.c < 0) tr.negCto = true; tr.dur.push(smp.d); tr.sz.push(smp.s); tr.sync.push(!(smp.f & 0x10000)); tr.cto.push(smp.c); tr.dts += smp.d; total += smp.s; } if (n) tr.chunks.push({ pos, n }); return pos + total; }; const readTraf = (b, traf, tracks, mo, ti, lastEnd) => { const tf = kids(b, traf.d, traf.e); const tfhd = kid(tf, 'tfhd'); req('no tfhd', tfhd); const tr = tracks[rd32(b, tfhd.d + 4)]; req('unknown track', tr); const th = readTfhd(b, tfhd, tr.trex); const base = fragBase(th, ti === 0, mo.s, lastEnd); applyTfdt(b, tf, tr); let nextPos = base; for (const run of tf.filter(x => x.t === 'trun')) { nextPos = readTrun(b, run, base, nextPos, th, tr); lastEnd = Math.max(lastEnd, nextPos); } return lastEnd; }; const readMoof = async (blob, mo, tracks) => { const b = new Uint8Array(await blob.slice(mo.s, mo.e).arrayBuffer()); const mf = kids(b, mo.d - mo.s, b.length); let lastEnd = 0; mf.filter(x => x.t === 'traf').forEach((traf, ti) => { lastEnd = readTraf(b, traf, tracks, mo, ti, lastEnd); }); }; const findMdat = (mdats, pos) => { let lo = 0, hi = mdats.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (pos < mdats[mid].d) hi = mid - 1; else if (pos >= mdats[mid].e) lo = mid + 1; else return mid; } return -1; }; const checkOverrun = (tr, mdats) => { let si = 0; for (const ch of tr.chunks) { let bytes = 0; for (let i = 0; i < ch.n; i++) bytes += tr.sz[si + i]; si += ch.n; if (ch.pos + bytes > mdats[ch.k].e) throw new Error('chunk overruns mdat'); } }; const mapChunks = (order, mdats) => { let payload = 0; const mdOff = mdats.map(m => { const o = payload; payload += m.e - m.d; return o; }); for (const tr of order) for (const ch of tr.chunks) { ch.k = findMdat(mdats, ch.pos); if (ch.k < 0) throw new Error('chunk outside mdat'); } order.forEach(tr => checkOverrun(tr, mdats)); return { payload, mdOff }; }; const stssBox = sync => { const idx = []; sync.forEach((s, i) => { if (s) idx.push(i + 1); }); return fullBox('stss', 0, 0, words([idx.length], 1), words(idx, 1)); }; const stscBox = chunks => { const sc = []; let prev = -1; chunks.forEach((ch, i) => { if (ch.n !== prev) { sc.push([i + 1, ch.n, 1]); prev = ch.n; } }); return fullBox('stsc', 0, 0, words([sc.length], 1), words(sc, 3)); }; const stcoBox = (tr, mp, payloadStart) => { const offs = tr.chunks.map(ch => payloadStart + mp.mdOff[ch.k] + (ch.pos - mp.mdats[ch.k].d)); return mp.use64 ? fullBox('co64', 0, 0, words([offs.length], 1), u8cat(offs.map(w64))) : fullBox('stco', 0, 0, words([offs.length], 1), words(offs, 1)); }; const buildStbl = (tr, mp, payloadStart) => { const parts = [tr.stsd, rleBox('stts', 0, tr.dur)]; if (tr.cto.some(v => v !== 0)) parts.push(rleBox('ctts', tr.negCto ? 1 : 0, tr.cto)); if (!tr.sync.every(Boolean)) parts.push(stssBox(tr.sync)); parts.push(stscBox(tr.chunks), fullBox('stsz', 0, 0, w32(0), w32(tr.sz.length), words(tr.sz, 1)), stcoBox(tr, mp, payloadStart)); return box('stbl', ...parts); }; const buildTrak = (tr, mp, payloadStart) => { const { moov, movTs } = mp; const sum = tr.dur.reduce((a, v) => a + v, 0); const t0 = movTs ? Math.round(tr.start / tr.ts * movTs) : 0; const tdur = movTs ? Math.round(sum / tr.ts * movTs) : 0; const minf = box('minf', ...rest(moov, tr.mi, ['stbl']), buildStbl(tr, mp, payloadStart)); const mdia = box('mdia', patchDur(moov, tr.mdhd, tr.durOffM, tr.dv1, sum), ...rest(moov, tr.mk2, ['mdhd', 'minf']), minf); const tkhd = patchDur(moov, tr.tkhd, tr.durOffT, tr.tv1, t0 + tdur); const edts = t0 > 0 ? [box('edts', fullBox('elst', 0, 0, w32(2), w32(t0), w32(0xFFFFFFFF), w32(0x00010000), w32(tdur), w32(0), w32(0x00010000)))] : []; return { trak: box('trak', tkhd, ...edts, ...rest(moov, tr.tk, ['tkhd', 'mdia', 'edts']), mdia), end: t0 + tdur }; }; const buildMoov = (mp, payloadStart) => { const { moov, mk, mvhd, mv1 } = mp; const built = mp.order.map(tr => buildTrak(tr, mp, payloadStart)); const movDur = built.reduce((m, x) => Math.max(m, x.end), 0); const mvh = patchDur(moov, mvhd, mvhd.d + 4 + (mv1 ? 16 : 8) + 4, mv1, movDur); return box('moov', mvh, ...rest(moov, mk, ['mvhd', 'trak', 'mvex']), ...built.map(x => x.trak)); }; const mp4 = async blob => { const size = blob.size; const top = await scanTop(blob, size); const moofs = top.filter(x => x.t === 'moof'), mdats = top.filter(x => x.t === 'mdat'); const moovTop = top.find(x => x.t === 'moov'); if (!moovTop) throw new Error('no moov'); if (!moofs.length) return blob; const moov = new Uint8Array(await blob.slice(moovTop.s, moovTop.e).arrayBuffer()); const mk = kids(moov, moovTop.d - moovTop.s, moov.length); const mvhd = kid(mk, 'mvhd'); req('no mvhd', mvhd); const mv1 = moov[mvhd.d] === 1, movTs = rd32(moov, mvhd.d + 4 + (mv1 ? 16 : 8)); const { tracks, order } = parseTracks(moov, mk, readTrex(moov, mk)); for (const mo of moofs) await readMoof(blob, mo, tracks); if (order.some(tr => !tr.dur.length)) throw new Error('empty track'); const { payload, mdOff } = mapChunks(order, mdats); const hdr = payload + 8 < 4294967296 ? 8 : 16; const use64 = size + moov.length + 4096 > 4294967295; const ftyp = box('ftyp', u8ascii('isom'), w32(512), u8ascii('isom'), u8ascii('iso2'), u8ascii('avc1'), u8ascii('mp41')); const mp = { moov, mk, mvhd, mv1, movTs, order, mdats, mdOff, use64 }; const m1 = buildMoov(mp, 0); const payloadStart = ftyp.length + m1.length + hdr; const m2 = buildMoov(mp, payloadStart); if (m2.length !== m1.length) throw new Error('moov size changed'); const mdatHead = hdr === 8 ? u8cat([w32(payload + 8), u8ascii('mdat')]) : u8cat([w32(1), u8ascii('mdat'), w64(payload + 16)]); return new Blob([ftyp, m2, mdatHead, ...mdats.map(m => blob.slice(m.d, m.e))], { type: 'video/mp4' }); }; const EBML = { HEADER: 0x1A45DFA3, SEGMENT: 0x18538067, CUES: 0x1C53BB6B, SEEKHEAD: 0x114D9B74, CLUSTER: 0x1F43B675, TRACKS: 0x1654AE6B, TRACK_ENTRY: 0xAE, TRACK_NUM: 0xD7, TRACK_TYPE: 0x83, TC: 0xE7, SBLOCK: 0xA3, GROUP: 0xA0, BLOCK: 0xA1, REFBLOCK: 0xFB }; const CLUSTER_END = new Set([EBML.CLUSTER, EBML.CUES, 0x1254C367, 0x1043A770]); const vintLen = (b, max) => { let l = 1; while (l <= max && !(b & (0x80 >> (l - 1)))) l++; return l; }; const ebmlReader = blob => { const size = blob.size; let ws = 0, wb = new Uint8Array(0); const need = async (p, n) => { if (p + n > size) return false; if (p >= ws && p + n <= ws + wb.length) return true; ws = p; wb = new Uint8Array(await blob.slice(p, Math.min(size, p + Math.max(n, 4194304))).arrayBuffer()); return true; }; const needAny = async (p, a, b) => (await need(p, a)) || (await need(p, b)); const at = p => wb[p - ws]; const uintAt = (p, n) => { let v = 0; for (let i = 0; i < n; i++) v = v * 256 + at(p + i); return v; }; const readId = p => { const l = vintLen(at(p), 4); return l > 4 ? null : { len: l, v: uintAt(p, l) }; }; const readSize = p => { const b = at(p), l = vintLen(b, 8); if (l > 8) return null; const m = 0xff >> l; let v = b & m, ones = v === m; for (let i = 1; i < l; i++) { const x = at(p + i); v = v * 256 + x; if (x !== 255) ones = false; } return { len: l, v, unk: ones }; }; const readEl = async p => { if (!(await needAny(p, 24, 2))) return null; const id = readId(p); if (!id) return null; if (!(await needAny(p, id.len + 8, id.len + 1))) return null; const sz = readSize(p + id.len); return sz && { id: id.v, hdr: id.len + sz.len, size: sz.v, unk: sz.unk, p }; }; const blockHead = async pl => { if (!(await need(pl, 4))) return null; const b0 = at(pl), l = vintLen(b0, 8); if (!(await need(pl, l + 3))) return null; let tn = b0 & (0xff >> l); for (let i = 1; i < l; i++) tn = tn * 256 + at(pl + i); const rel = (at(pl + l) << 8) | at(pl + l + 1); return { tn, rel: rel > 32767 ? rel - 65536 : rel, flags: at(pl + l + 2) }; }; return { size, readEl, uintAt, blockHead }; }; const readTrackEntry = async (rd, q, te) => { let num = 0, type = 0, r = q + te.hdr; const tend = r + te.size; while (r < tend) { const ce = await rd.readEl(r); if (!ce) break; if (ce.id === EBML.TRACK_NUM) num = rd.uintAt(r + ce.hdr, ce.size); if (ce.id === EBML.TRACK_TYPE) type = rd.uintAt(r + ce.hdr, ce.size); r += ce.hdr + ce.size; } return { num, type }; }; const videoTrackIn = async (rd, el) => { const end = el.p + el.hdr + el.size; let q = el.p + el.hdr, found = 0; while (q < end) { const te = await rd.readEl(q); if (!te) break; if (te.id === EBML.TRACK_ENTRY && !found) { const { num, type } = await readTrackEntry(rd, q, te); if (type === 1) found = num; } q += te.hdr + te.size; } return found; }; const headerStop = (el, videoTrack, p) => { if (el.id === EBML.CUES || el.id === EBML.SEEKHEAD) return { r: null }; if (el.id === EBML.CLUSTER) return { r: videoTrack ? { videoTrack, firstCluster: p } : null }; if (el.unk) throw new Error('unknown size header element'); return null; }; const scanHeader = async (rd, start) => { let p = start, videoTrack = 0; while (p < rd.size) { const el = await rd.readEl(p); if (!el) throw new Error('bad element'); const stop = headerStop(el, videoTrack, p); if (stop) return stop.r; if (el.id === EBML.TRACKS) videoTrack ||= await videoTrackIn(rd, el); p += el.hdr + el.size; } return null; }; const readGroup = async (rd, pl, ch) => { let r = pl, h = null, ref = false; const gend = pl + ch.size; while (r < gend) { const ge = await rd.readEl(r); if (!ge) break; if (ge.id === EBML.BLOCK) h = await rd.blockHead(r + ge.hdr); else if (ge.id === EBML.REFBLOCK) ref = true; r += ge.hdr + ge.size; } return { h, ref }; }; const keyframeRel = async (rd, ch, pl, videoTrack) => { if (ch.id === EBML.SBLOCK) { const h = await rd.blockHead(pl); return h && h.tn === videoTrack && (h.flags & 0x80) ? h.rel : null; } if (ch.id === EBML.GROUP) { const { h, ref } = await readGroup(rd, pl, ch); return h && h.tn === videoTrack && !ref ? h.rel : null; } return null; }; const scanChild = async (rd, ch, pl, st, cp, addCue) => { if (ch.id === EBML.TC) { st.tc = rd.uintAt(pl, ch.size); return; } const rel = await keyframeRel(rd, ch, pl, st.videoTrack); if (rel !== null) addCue(st.tc + rel, cp); }; const scanCluster = async (rd, cl, cp, videoTrack, addCue) => { let q = cp + cl.hdr; const st = { tc: 0, videoTrack }; const cend = cl.unk ? rd.size : cp + cl.hdr + cl.size; while (q < cend) { const ch = await rd.readEl(q); if (!ch) return rd.size; if (CLUSTER_END.has(ch.id)) break; if (ch.unk) throw new Error('unknown size child'); const pl = q + ch.hdr; await scanChild(rd, ch, pl, st, cp, addCue); q = pl + ch.size; } return q; }; const collectCues = async (rd, firstCluster, videoTrack, sds) => { const cues = []; let lastT = -1e9; const addCue = (t, pos) => { if (!cues.length || t - lastT >= 500) { cues.push({ t: Math.max(0, t), pos: pos - sds }); lastT = t; } }; let cp = firstCluster; while (cp < rd.size) { const cl = await rd.readEl(cp); if (!cl || cl.id !== EBML.CLUSTER) break; cp = await scanCluster(rd, cl, cp, videoTrack, addCue); } return cues; }; const writeCues = (cues, videoTrack, total) => { const out = new Uint8Array(total), dv = new DataView(out.buffer); out.set([0x1C, 0x53, 0xBB, 0x6B, 0x01], 0); dv.setUint32(8, 19 * cues.length); let o = 12; for (const c of cues) { out.set([0xBB, 0x91, 0xB3, 0x84], o); dv.setUint32(o + 4, c.t); out.set([0xB7, 0x89, 0xF7, 0x81, videoTrack & 255, 0xF1, 0x84], o + 8); dv.setUint32(o + 15, c.pos + total); o += 19; } return out; }; const intactEnd = async (rd, p) => { let ok = p; while (p < rd.size) { const cl = await rd.readEl(p); if (!cl || cl.id !== EBML.CLUSTER) break; if (!cl.unk) { const e = p + cl.hdr + cl.size; if (e > rd.size) break; ok = p = e; continue; } let q = p + cl.hdr; ok = q; while (q < rd.size) { const ch = await rd.readEl(q); if (!ch || CLUSTER_END.has(ch.id) || ch.unk) break; const e = q + ch.hdr + ch.size; if (e > rd.size) return ok; ok = q = e; } p = q; } return ok; }; const webm = async blob => { const rd = ebmlReader(blob), size = blob.size; const eh = await rd.readEl(0); if (!eh || eh.id !== EBML.HEADER) throw new Error('no EBML'); const sp = eh.hdr + eh.size; const seg = await rd.readEl(sp); if (!seg || seg.id !== EBML.SEGMENT) throw new Error('no segment'); if (!seg.unk) return blob; const sds = sp + seg.hdr; const info = await scanHeader(rd, sds); if (!info) return blob; const { videoTrack, firstCluster } = info; const cues = await collectCues(rd, firstCluster, videoTrack, sds); if (!cues.length) return blob; const total = 12 + 19 * cues.length; if (size - sds + total > 4294967295) return blob; const out = writeCues(cues, videoTrack, total); const cut = await intactEnd(rd, firstCluster); return new Blob([blob.slice(0, firstCluster), out, cut > firstCluster && cut < size ? blob.slice(firstCluster, cut) : blob.slice(firstCluster)], { type: blob.type }); }; const clusterEnd = (rd, cl, cp) => cl.unk ? rd.size : cp + cl.hdr + cl.size; const blockHeadOf = async (rd, ch, pl) => { if (ch.id === EBML.SBLOCK) return rd.blockHead(pl); if (ch.id === EBML.GROUP) return (await readGroup(rd, pl, ch)).h; return null; }; const clusterMax = async (rd, cl, cp) => { let q = cp + cl.hdr, tc = 0, max = null; const cend = clusterEnd(rd, cl, cp); while (q < cend) { const ch = await rd.readEl(q); if (!ch) { q = rd.size; break; } if (CLUSTER_END.has(ch.id)) break; if (ch.unk) throw new Error('unknown size child'); const pl = q + ch.hdr; if (ch.id === EBML.TC) tc = rd.uintAt(pl, ch.size); else { const h = await blockHeadOf(rd, ch, pl); if (h) max = Math.max(max === null ? -1 : max, tc + h.rel); } q = pl + ch.size; } return { end: q, max: max === null ? tc : max }; }; const readSegment = async rd => { const eh = await rd.readEl(0); if (!eh || eh.id !== EBML.HEADER) return null; const seg = await rd.readEl(eh.hdr + eh.size); return seg && seg.id === EBML.SEGMENT ? seg : null; }; const readTimescale = async (rd, info, scale) => { let q = info.p + info.hdr; const end = q + info.size; while (q < end) { const ce = await rd.readEl(q); if (!ce) break; if (ce.id === 0x2AD7B1) scale = rd.uintAt(q + ce.hdr, ce.size); q += ce.hdr + ce.size; } return scale; }; const scanSegmentHead = async (rd, seg) => { let p = seg.p + seg.hdr, scale = 1e6; while (p < rd.size) { const el = await rd.readEl(p); if (!el) return null; if (el.id === EBML.CLUSTER) break; if (el.unk) return null; if (el.id === 0x1549A966) scale = await readTimescale(rd, el, scale); p += el.hdr + el.size; } return { p, scale }; }; const lastCluster = async (rd, p) => { let last = null; while (p < rd.size) { const cl = await rd.readEl(p); if (!cl || cl.id !== EBML.CLUSTER) break; last = { cl, p }; if (cl.unk) { const r = await clusterMax(rd, cl, p); last.max = r.max; p = r.end; } else p += cl.hdr + cl.size; } return last; }; const webmDuration = async blob => { const rd = ebmlReader(blob); const seg = await readSegment(rd); if (!seg) return null; const head = await scanSegmentHead(rd, seg); if (!head) return null; const last = await lastCluster(rd, head.p); if (!last) return null; if (last.max === undefined) last.max = (await clusterMax(rd, last.cl, last.p)).max; const ms = last.max * head.scale / 1e6; return ms > 0 ? ms : null; }; const blankSeekHead = async (blob, p, tot) => { if (p + tot > WEBM_HEAD) return blob; const head = new Uint8Array(await blob.slice(0, WEBM_HEAD).arrayBuffer()); const small = tot - 3 < 16384, hl = small ? 3 : 9, len = tot - hl; head[p] = 0xEC; if (small) { head[p + 1] = 0x40 | (len >> 8); head[p + 2] = len & 255; } else { head[p + 1] = 0x01; head.fill(0, p + 2, p + 5); new DataView(head.buffer).setUint32(p + 5, len); } head.fill(0, p + hl, p + tot); return new Blob([head, blob.slice(WEBM_HEAD)], { type: blob.type }); }; const stripSeekHead = async blob => { const rd = ebmlReader(blob); const seg = await readSegment(rd); if (!seg || !seg.unk) return blob; let p = seg.p + seg.hdr; while (p < Math.min(rd.size, WEBM_HEAD)) { const el = await rd.readEl(p); if (!el || el.unk || el.id === EBML.CLUSTER) return blob; const tot = el.hdr + el.size; if (el.id === EBML.SEEKHEAD) return blankSeekHead(blob, p, tot); p += tot; } return blob; }; return { mp4, webm, webmDuration, stripSeekHead }; })(); const HlsKit = (() => { const TWO32 = 4294967296, TWO33 = 8589934592; const be = (n, len) => { const o = new Uint8Array(len); let v = n; for (let i = len - 1; i >= 0; i--) { o[i] = v % 256; v = Math.floor(v / 256); } return o; }; const b32 = n => be(n >>> 0, 4); const z = n => new Uint8Array(n); const box = (t, ...p) => { const body = u8cat(p), o = new Uint8Array(8 + body.length); new DataView(o.buffer).setUint32(0, o.length); o.set(u8ascii(t), 4); o.set(body, 8); return o; }; const fbox = (t, ver, fl, ...p) => box(t, Uint8Array.of(ver, (fl >> 16) & 255, (fl >> 8) & 255, fl & 255), ...p); const MATRIX = u8cat([b32(0x10000), z(8), b32(0), b32(0x10000), z(8), b32(0), b32(0x40000000)].flatMap(x => [x])); const eq = (a, b) => !!a && !!b && a.length === b.length && a.every((v, i) => v === b[i]); class NeedNewInit extends Error {} const unescapeRbsp = (nal, skip = 1) => { const rb = []; let zeros = 0; for (let i = skip; i < nal.length; i++) { const b = nal[i]; if (zeros >= 2 && b === 3) { zeros = 0; continue; } zeros = b === 0 ? zeros + 1 : 0; rb.push(b); } return rb; }; const bitReader = rb => { let pos = 0; const u = n => { let v = 0; for (let i = 0; i < n; i++, pos++) v = v * 2 + ((rb[pos >> 3] >> (7 - (pos & 7))) & 1); return v; }; const ue = () => { let l = 0; while (l < 32 && u(1) === 0) l++; return l ? (2 ** l - 1) + u(l) : 0; }; const se = () => { const k = ue(); return k & 1 ? (k + 1) / 2 : -(k / 2); }; return { u, ue, se }; }; const HIGH_PROFILES = [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135]; const skipScalingList = (r, size) => { let last = 8, next = 8; for (let j = 0; j < size; j++) { if (next) next = (last + r.se() + 256) % 256; last = next || last; } }; const skipScalingMatrix = (r, chroma) => { for (let i = 0; i < (chroma !== 3 ? 8 : 12); i++) if (r.u(1)) skipScalingList(r, i < 6 ? 16 : 64); }; const readHighProfile = (r, prof) => { const out = { chroma: 1, bdl: 8, bdc: 8 }; if (!HIGH_PROFILES.includes(prof)) return out; out.chroma = r.ue(); if (out.chroma === 3) r.u(1); out.bdl = 8 + r.ue(); out.bdc = 8 + r.ue(); r.u(1); if (r.u(1)) skipScalingMatrix(r, out.chroma); return out; }; const skipPocType = r => { const poc = r.ue(); if (poc === 0) r.ue(); else if (poc === 1) { r.u(1); r.se(); r.se(); const n = r.ue(); for (let i = 0; i < n; i++) r.se(); } }; const readCrop = r => r.u(1) ? { cl: r.ue(), cr: r.ue(), ct: r.ue(), cb: r.ue() } : { cl: 0, cr: 0, ct: 0, cb: 0 }; const parseSps = nal => { const r = bitReader(unescapeRbsp(nal)); const prof = r.u(8), compat = r.u(8), level = r.u(8); r.ue(); const { chroma, bdl, bdc } = readHighProfile(r, prof); r.ue(); skipPocType(r); r.ue(); r.u(1); const wm = r.ue(), hm = r.ue(), fmo = r.u(1); if (!fmo) r.u(1); r.u(1); const { cl, cr, ct, cb } = readCrop(r); const cux = chroma === 1 || chroma === 2 ? 2 : 1, cuy = (chroma === 1 ? 2 : 1) * (2 - fmo); const w = (wm + 1) * 16 - cux * (cl + cr), h = (2 - fmo) * (hm + 1) * 16 - cuy * (ct + cb); if (!(w > 0 && h > 0 && w < 16384 && h < 16384)) throw new Error('bad sps'); return { prof, compat, level, chroma, bdl, bdc, w, h }; }; const parseHevcPtl = r => { const space = r.u(2), tier = r.u(1), prof = r.u(5); const compat = [r.u(8), r.u(8), r.u(8), r.u(8)]; const cons = [r.u(8), r.u(8), r.u(8), r.u(8), r.u(8), r.u(8)]; return { space, tier, prof, compat, cons, level: r.u(8) }; }; const skipHevcSubLayers = (r, n) => { const pp = [], lp = []; for (let i = 0; i < n; i++) { pp.push(r.u(1)); lp.push(r.u(1)); } if (n > 0) for (let i = n; i < 8; i++) r.u(2); for (let i = 0; i < n; i++) { if (pp[i]) for (let k = 0; k < 11; k++) r.u(8); if (lp[i]) r.u(8); } }; const parseHevcSps = nal => { const r = bitReader(unescapeRbsp(nal, 2)); r.u(4); const sub = r.u(3), nest = r.u(1); const ptl = parseHevcPtl(r); skipHevcSubLayers(r, sub); r.ue(); const chroma = r.ue(); if (chroma === 3) r.u(1); const pw = r.ue(), ph = r.ue(); let cl = 0, cr = 0, ct = 0, cb = 0; if (r.u(1)) { cl = r.ue(); cr = r.ue(); ct = r.ue(); cb = r.ue(); } const bdl = 8 + r.ue(), bdc = 8 + r.ue(); const sw = chroma === 1 || chroma === 2 ? 2 : 1, sh = chroma === 1 ? 2 : 1; const w = pw - sw * (cl + cr), h = ph - sh * (ct + cb); if (!(w > 0 && h > 0 && w < 16384 && h < 16384)) throw new Error('bad hevc sps'); return { codec: 'hevc', ...ptl, chroma, bdl, bdc, w, h, layers: sub + 1, nest }; }; const hevcCodecString = v => { const hex = n => n.toString(16); const fl = ((v.compat[0] << 24) | (v.compat[1] << 16) | (v.compat[2] << 8) | v.compat[3]) >>> 0; let rev = 0; for (let i = 0; i < 32; i++) if (fl & (1 << i)) rev |= 1 << (31 - i); const c = v.cons.slice(); while (c.length > 1 && c[c.length - 1] === 0) c.pop(); return `hvc1.${['', 'A', 'B', 'C'][v.space]}${v.prof}.${hex(rev >>> 0)}.${v.tier ? 'H' : 'L'}${v.level}.${c.map(x => hex(x).toUpperCase()).join('.')}`; }; const AAC_SR = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; const tsTime = (b, o) => (b[o] & 0x0e) * 536870912 + b[o + 1] * 4194304 + (b[o + 2] & 0xfe) * 16384 + b[o + 3] * 128 + (b[o + 4] >> 1); const splitNals = b => { const out = [], n = b.length; let i = 0, start = -1; const push = e => { while (e > start && b[e - 1] === 0) e--; if (e > start) out.push(b.subarray(start, e)); }; while (i < n - 2) { if (b[i + 2] > 1) i += 3; else if (b[i] === 0 && b[i + 1] === 0 && b[i + 2] === 1) { if (start >= 0) push(i); start = i + 3; i += 3; } else i++; } if (start >= 0) push(n); return out; }; const findTsSync = buf => { let p = 0; while (p + 376 <= buf.length && !(buf[p] === 0x47 && buf[p + 188] === 0x47)) p++; return p; }; const parsePes = d => { if (d.length < 9 || d[0] || d[1] || d[2] !== 1) return null; const fl = d[7] >> 6, hl = d[8]; if (!(fl & 2) || d.length < 9 + hl) return null; const pts = tsTime(d, 9); return { pts, dts: fl === 3 ? tsTime(d, 14) : pts, pay: d.subarray(9 + hl) }; }; const collectNals = (nals, res) => { const keep = []; let key = false; for (const nl of nals) { const t = nl[0] & 31; if (t === 7) { if (!res.sps) res.sps = nl.slice(); } else if (t === 8) { if (!res.pps) res.pps = nl.slice(); } else if (t >= 1 && t <= 5) { keep.push(nl); if (t === 5) key = true; } } return { keep, key }; }; const collectHevcNals = (nals, res) => { const keep = []; let key = false; for (const nl of nals) { if (nl.length < 3 || ((nl[0] & 1) << 5 | nl[1] >> 3) !== 0) continue; const t = (nl[0] >> 1) & 63; if (t === 32) { if (!res.vps) res.vps = nl.slice(); } else if (t === 33) { if (!res.sps) res.sps = nl.slice(); } else if (t === 34) { if (!res.pps) res.pps = nl.slice(); } else if (t <= 21) { keep.push(nl); if (t >= 16) key = true; } } return { keep, key }; }; const avcSample = keep => { let n = 0; for (const k of keep) n += 4 + k.length; const data = new Uint8Array(n); let q = 0; for (const k of keep) { new DataView(data.buffer).setUint32(q, k.length); data.set(k, q + 4); q += 4 + k.length; } return data; }; const addVideo = (res, pay, pts, dts) => { const nals = splitNals(pay); const { keep, key } = res.codec === 'hevc' ? collectHevcNals(nals, res) : collectNals(nals, res); if (!keep.length) return; res.video.push({ pts, dts, key, data: avcSample(keep) }); }; const parseAdts = (pay, q) => { const prot = pay[q + 1] & 1, prof = (pay[q + 2] >> 6) & 3, sfi = (pay[q + 2] >> 2) & 15; const ch = ((pay[q + 2] & 1) << 2) | (pay[q + 3] >> 6), fl = ((pay[q + 3] & 3) << 11) | (pay[q + 4] << 3) | (pay[q + 5] >> 5); return { prof, sfi, ch, fl, hl: prot ? 7 : 9 }; }; const addAudio = (res, pay, pts) => { let q = 0, i = 0; while (q + 7 <= pay.length) { if (pay[q] !== 0xff || (pay[q + 1] & 0xf0) !== 0xf0) { q++; continue; } const f = parseAdts(pay, q); if (f.sfi > 12 || f.fl < f.hl || q + f.fl > pay.length) break; const sr = AAC_SR[f.sfi]; if (!res.aac) res.aac = { aot: f.prof + 1, sfi: f.sfi, sr, ch: f.ch || 2 }; res.audio.push({ pts: pts + i * 1024 * 90000 / sr, data: pay.subarray(q + f.hl, q + f.fl) }); i++; q += f.fl; } }; const flushPes = (st, pid) => { const pe = st.pes.get(pid); if (!pe || !pe.chunks.length) return; const d = u8cat(pe.chunks); pe.chunks = []; const pes = parsePes(d); if (!pes) return; if (pid === st.vPid) addVideo(st.res, pes.pay, pes.pts, pes.dts); else if (st.aType === 0x0f) addAudio(st.res, pes.pay, pes.pts); }; const parsePat = (buf, s, end, st) => { for (let o = s + 8; o + 4 <= end; o += 4) { const prog = (buf[o] << 8) | buf[o + 1]; if (prog) { st.pmtPid = ((buf[o + 2] & 0x1f) << 8) | buf[o + 3]; break; } } }; const parsePmt = (buf, s, end, st) => { let o = s + 12 + (((buf[s + 10] & 0x0f) << 8) | buf[s + 11]); while (o + 5 <= end) { const type = buf[o], sp = ((buf[o + 1] & 0x1f) << 8) | buf[o + 2]; if (type === 0x1b && st.vPid < 0) { st.vPid = sp; st.res.codec = 'avc'; } else if (type === 0x0f && st.aPid < 0) { st.aPid = sp; st.aType = type; } else if (type === 0x24 && st.vPid < 0) { st.vPid = sp; st.res.codec = 'hevc'; } o += 5 + (((buf[o + 3] & 0x0f) << 8) | buf[o + 4]); } }; const parsePsi = (buf, p, q, pid, st) => { const s = q + 1 + buf[q], len = ((buf[s + 1] & 0x0f) << 8) | buf[s + 2], end = Math.min(p + 188, s + 3 + len - 4); if (pid === 0 && buf[s] === 0) parsePat(buf, s, end, st); else if (pid === st.pmtPid && buf[s] === 2) parsePmt(buf, s, end, st); }; const tsPayloadOffset = (buf, p) => { const afc = (buf[p + 3] >> 4) & 3; let q = p + 4; if (afc & 2) q += 1 + buf[p + 4]; return !(afc & 1) || q >= p + 188 ? -1 : q; }; const addPesChunk = (st, pid, pusi, chunk) => { if (pusi) flushPes(st, pid); let pe = st.pes.get(pid); if (!pe) st.pes.set(pid, pe = { chunks: [] }); if (pusi || pe.chunks.length) pe.chunks.push(chunk); }; const tsPacket = (buf, p, st) => { const q = tsPayloadOffset(buf, p); if (q < 0) return; const pusi = buf[p + 1] & 0x40, pid = ((buf[p + 1] & 0x1f) << 8) | buf[p + 2]; if (pid === 0 || pid === st.pmtPid) { if (pusi) parsePsi(buf, p, q, pid, st); return; } if (pid === st.vPid || pid === st.aPid) addPesChunk(st, pid, pusi, buf.subarray(q, p + 188)); }; const demuxTs = buf => { const st = { res: { video: [], audio: [], sps: null, pps: null, vps: null, codec: 'avc', aac: null }, pmtPid: -1, vPid: -1, aPid: -1, aType: 0, pes: new Map() }; for (let p = findTsSync(buf); p + 188 <= buf.length; p += 188) { if (buf[p] !== 0x47) { const nx = buf.indexOf(0x47, p + 1); if (nx < 0) break; p = nx - 188; continue; } tsPacket(buf, p, st); } flushPes(st, st.vPid); flushPes(st, st.aPid); return st.res; }; const desc = (tag, ...p) => { const b = u8cat(p); if (b.length > 127) throw new Error('desc too long'); return u8cat([Uint8Array.of(tag, b.length), b]); }; const esds = a => fbox('esds', 0, 0, desc(3, be(0, 2), Uint8Array.of(0), desc(4, Uint8Array.of(0x40, 0x15), be(0, 3), be(0, 4), be(0, 4), desc(5, a)), desc(6, Uint8Array.of(2)))); const avcC = v => { const parts = [Uint8Array.of(1, v.prof, v.compat, v.level, 0xff, 0xe1), be(v.sps.length, 2), v.sps, Uint8Array.of(1), be(v.pps.length, 2), v.pps]; if ([100, 110, 122, 144].includes(v.prof)) parts.push(Uint8Array.of(0xfc | v.chroma, 0xf8 | (v.bdl - 8), 0xf8 | (v.bdc - 8), 0)); return box('avcC', ...parts); }; const hvcC = v => { const arr = (type, n) => [Uint8Array.of(0x80 | type, 0, 1), be(n.length, 2), n]; return box('hvcC', Uint8Array.of(1, (v.space << 6) | (v.tier << 5) | v.prof), Uint8Array.from(v.compat), Uint8Array.from(v.cons), Uint8Array.of(v.level, 0xf0, 0x00, 0xfc, 0xfc | v.chroma, 0xf8 | (v.bdl - 8), 0xf8 | (v.bdc - 8), 0, 0, (v.layers << 3) | (v.nest << 2) | 3, 3), ...arr(32, v.vps), ...arr(33, v.sps), ...arr(34, v.pps)); }; const trakBox = (id, ts, kind, entry, w, h) => box('trak', fbox('tkhd', 0, 7, b32(0), b32(0), b32(id), b32(0), b32(0), z(8), be(0, 2), be(0, 2), be(kind === 'soun' ? 0x0100 : 0, 2), z(2), MATRIX, b32(w * 65536), b32(h * 65536)), box('mdia', fbox('mdhd', 0, 0, b32(0), b32(0), b32(ts), b32(0), be(0x55c4, 2), z(2)), fbox('hdlr', 0, 0, b32(0), u8ascii(kind), z(12), u8ascii(kind === 'vide' ? 'Video\0' : 'Audio\0')), box('minf', kind === 'vide' ? fbox('vmhd', 0, 1, z(8)) : fbox('smhd', 0, 0, z(4)), box('dinf', fbox('dref', 0, 0, b32(1), fbox('url ', 0, 1))), box('stbl', fbox('stsd', 0, 0, b32(1), entry), fbox('stts', 0, 0, b32(0)), fbox('stsc', 0, 0, b32(0)), fbox('stsz', 0, 0, b32(0), b32(0)), fbox('stco', 0, 0, b32(0)))))); const buildInit = (v, a) => { const hevc = v.codec === 'hevc'; const avc1 = box(hevc ? 'hvc1' : 'avc1', z(6), be(1, 2), z(16), be(v.w, 2), be(v.h, 2), b32(0x480000), b32(0x480000), z(4), be(1, 2), z(32), be(0x18, 2), be(0xffff, 2), hevc ? hvcC(v) : avcC(v)); const traks = [trakBox(1, 90000, 'vide', avc1, v.w, v.h)]; const trex = [fbox('trex', 0, 0, b32(1), b32(1), b32(0), b32(0), b32(0))]; if (a) { const ascb = be(((a.aot << 11) | (a.sfi << 7) | (a.ch << 3)), 2); const mp4a = box('mp4a', z(6), be(1, 2), z(8), be(a.ch, 2), be(16, 2), z(4), be(a.sr < 65536 ? a.sr * 65536 : 0, 4), esds(ascb)); traks.push(trakBox(2, a.sr, 'soun', mp4a, 0, 0)); trex.push(fbox('trex', 0, 0, b32(2), b32(1), b32(0), b32(0), b32(0))); } return u8cat([ box('ftyp', u8ascii('isom'), b32(512), u8ascii('isom'), u8ascii('iso5'), u8ascii('iso6'), u8ascii(v.codec === 'hevc' ? 'hvc1' : 'avc1'), u8ascii('mp41')), box('moov', fbox('mvhd', 0, 0, b32(0), b32(0), b32(1000), b32(0), b32(0x10000), be(0x0100, 2), z(10), MATRIX, z(24), b32(a ? 3 : 2)), ...traks, box('mvex', ...trex)) ]); }; const buildFragment = (seq, tracks) => { const mk = base => { let off = base; const trafs = tracks.map(t => { const run = fbox('trun', 1, 0xf01, b32(t.samples.length), b32(off), ...t.samples.map(s => u8cat([b32(s.dur), b32(s.data.length), b32(s.flags), b32(s.cts)]))); off += t.samples.reduce((n, s) => n + s.data.length, 0); return box('traf', fbox('tfhd', 0, 0x20000, b32(t.id)), fbox('tfdt', 1, 0, be(t.t, 8)), run); }); return box('moof', fbox('mfhd', 0, 0, b32(seq)), ...trafs); }; const moofLen = mk(0).length, moof = mk(moofLen + 8); let n = 0; for (const t of tracks) for (const s of t.samples) n += s.data.length; const mdat = new Uint8Array(8 + n); new DataView(mdat.buffer).setUint32(0, mdat.length); mdat.set(u8ascii('mdat'), 4); let q = 8; for (const t of tracks) for (const s of t.samples) { mdat.set(s.data, q); q += s.data.length; } return u8cat([moof, mdat]); }; const KEY_FLAGS = 0x02000000, DELTA_FLAGS = 0x01010000; const unwrapper = () => { let off = 0, last = null; return v => { v += off; if (last !== null) { const d = v - last; if (d < -TWO32) { off += TWO33; v += TWO33; } else if (d > TWO32) { off -= TWO33; v -= TWO33; } } last = v; return v; }; }; const modCts = d => (((d + TWO32) % TWO33) + TWO33) % TWO33 - TWO32; const differs = (a, b) => !!a && !!b && !eq(a, b); const audioChanged = (a, b) => !!a && !!b && (a.sr !== b.sr || a.ch !== b.ch); class Fmp4Mux { constructor() { this.init = null; this.vi = null; this.ai = null; this.sps = null; this.pps = null; this.vps = null; this.codec = null; this.t0 = null; this.shift = 0; this.seq = 0; this.pend = null; this.lastDur = 3000; this.aNext = null; this.uwV = unwrapper(); this.uwA = unwrapper(); } _check(seg) { if (seg.sps && this.codec && seg.codec !== this.codec) throw new NeedNewInit('codec'); if (differs(seg.sps, this.sps) || differs(seg.pps, this.pps) || differs(seg.vps, this.vps)) throw new NeedNewInit('params'); if (audioChanged(seg.aac, this.ai)) throw new NeedNewInit('audio'); } _ensureInit(seg, out) { if (this.init) return true; const hevc = seg.codec === 'hevc'; if (!seg.sps || !seg.pps || (hevc && !seg.vps)) return false; this.sps = seg.sps; this.pps = seg.pps; this.vps = hevc ? seg.vps : null; this.codec = hevc ? 'hevc' : 'avc'; this.vi = { ...(hevc ? parseHevcSps(seg.sps) : parseSps(seg.sps)), sps: seg.sps, pps: seg.pps, vps: this.vps }; this.ai = seg.aac || null; this.init = buildInit(this.vi, this.ai); out.push(this.init); return true; } _align(vs, disc) { if (this.t0 === null) { const k = vs.findIndex(s => s.key); if (k < 0) return null; vs = vs.slice(k); this.t0 = vs[0].dts; } else if (vs.length) { const firstAdj = vs[0].dts - this.t0 + this.shift, exp = this.pend ? this.pend.adj + this.lastDur : firstAdj; if (disc || Math.abs(firstAdj - exp) > 180000) this.shift += exp - firstAdj; } return vs; } _videoTrack(vs) { const q = this.pend ? [this.pend, ...vs] : vs; if (!q.length) return null; this.pend = q[q.length - 1]; const samples = []; for (let i = 0; i < q.length - 1; i++) { let dur = q[i + 1].adj - q[i].adj; if (dur <= 0) dur = this.lastDur; else this.lastDur = dur; samples.push({ dur, cts: q[i].cts, flags: q[i].key ? KEY_FLAGS : DELTA_FLAGS, data: q[i].data }); } return samples.length ? { id: 1, t: Math.max(0, q[0].adj), samples } : null; } _audioTrack(au) { if (!this.ai || !au.length) return null; const sr = this.ai.sr, a = au.map(s => ({ t: (s.pts - this.t0 + this.shift) * sr / 90000, data: s.data })).filter(s => s.t >= -1024); if (!a.length) return null; const t0 = Math.round(Math.max(0, a[0].t)); const start = this.aNext === null || Math.abs(t0 - this.aNext) > sr * 0.04 ? t0 : this.aNext; this.aNext = start + a.length * 1024; return { id: 2, t: start, samples: a.map(s => ({ dur: 1024, cts: 0, flags: KEY_FLAGS, data: s.data })) }; } push(seg, disc) { const out = []; this._check(seg); if (!this._ensureInit(seg, out)) return out; let vs = seg.video.map(s => ({ dts: this.uwV(s.dts), cts: modCts(s.pts - s.dts), key: s.key, data: s.data })); const au = seg.audio.map(s => ({ pts: this.uwA(s.pts), data: s.data })); vs = this._align(vs, disc); if (!vs) return out; for (const s of vs) s.adj = s.dts - this.t0 + this.shift; const tracks = [this._videoTrack(vs), this._audioTrack(au)].filter(Boolean); if (tracks.length) out.push(buildFragment(++this.seq, tracks)); return out; } flush() { if (!this.pend) return []; const s = this.pend; this.pend = null; return [buildFragment(++this.seq, [{ id: 1, t: Math.max(0, s.adj), samples: [{ dur: this.lastDur, cts: s.cts, flags: s.key ? KEY_FLAGS : DELTA_FLAGS, data: s.data }] }])]; } } const attrs = s => { const o = {}, re = /([A-Z0-9-]+)=("[^"]*"|[^,]*)/g; let m; while ((m = re.exec(s))) o[m[1]] = m[2].replace(/^"|"$/g, ''); return o; }; const TAG_HANDLERS = { '#EXT-X-STREAM-INF': (s, val) => { s.sv = attrs(val); }, '#EXT-X-MEDIA': (s, val) => { const a = attrs(val); if (a.TYPE === 'AUDIO' && a.URI) s.res.audio.push({ url: s.abs(a.URI), group: a['GROUP-ID'] || '', def: a.DEFAULT === 'YES', auto: a.AUTOSELECT === 'YES' }); }, '#EXT-X-TARGETDURATION': (s, val) => { s.res.target = +val || 6; }, '#EXT-X-MEDIA-SEQUENCE': (s, val) => { s.seq = +val || 0; }, '#EXTINF': (s, val) => { s.dur = parseFloat(val) || 0; }, '#EXT-X-DISCONTINUITY': s => { s.disc = true; }, '#EXT-X-ENDLIST': s => { s.res.end = true; }, '#EXT-X-BYTERANGE': (s, val) => { const [n, o] = val.trim().split('@'); s.br = { n: +n, o: o === undefined ? null : +o }; }, '#EXT-X-KEY': (s, val) => { const a = attrs(val); s.key = a.METHOD === 'NONE' ? null : { method: a.METHOD, uri: a.URI ? s.abs(a.URI) : '', iv: a.IV || '' }; }, '#EXT-X-MAP': (s, val) => { const a = attrs(val), uri = s.abs(a.URI); let range = null; if (a.BYTERANGE) { const [n, o] = a.BYTERANGE.split('@').map(Number); if (!(n > 0)) throw new Error('bad byte-range'); range = { n, o: o || 0 }; } s.map = { uri, range, key: `${uri}#${range ? `${range.o}-${range.n}` : ''}` }; } }; const takeRange = (s, uri) => { if (!s.br) return null; const { n, o } = s.br; if (!(n > 0)) throw new Error('bad byte-range'); const off = o !== null && !Number.isNaN(o) ? o : (s.ends.get(uri) || 0); s.ends.set(uri, off + n); s.br = null; return { n, o: off }; }; const addUri = (s, l) => { if (s.sv) { const [w, h] = (s.sv.RESOLUTION || '').split('x').map(Number); s.res.variants.push({ url: s.abs(l), bw: +s.sv.BANDWIDTH || 0, w: w || 0, h: h || 0, aud: s.sv.AUDIO || '', codecs: s.sv.CODECS || '' }); s.res.master = true; s.sv = null; } else { const uri = s.abs(l); s.res.segs.push({ uri, seq: s.seq++, dur: s.dur, disc: s.disc, key: s.key, map: s.map, range: takeRange(s, uri) }); s.disc = false; } }; const parsePlaylist = (text, base) => { const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); if (!lines.length || !lines[0].startsWith('#EXTM3U')) throw new Error('not an m3u8'); const s = { abs: u => new URL(u, base).href, res: { master: false, variants: [], audio: [], segs: [], target: 6, end: false }, seq: 0, dur: 0, disc: false, key: null, map: null, sv: null, br: null, ends: new Map() }; for (const l of lines) { if (l[0] !== '#') { addUri(s, l); continue; } const c = l.indexOf(':'), tag = c < 0 ? l : l.slice(0, c), val = c < 0 ? '' : l.slice(c + 1); const handler = TAG_HANDLERS[tag]; if (handler) handler(s, val); } return s.res; }; const pickVariant = (vs, maxH) => { if (!vs.length) throw new Error('empty master playlist'); const withH = vs.filter(v => v.h); if (withH.length) { const ok = withH.filter(v => v.h <= maxH); return (ok.length ? ok : withH).sort((a, b) => ok.length ? (b.h - a.h || b.bw - a.bw) : (a.h - b.h || a.bw - b.bw))[0]; } const s = [...vs].sort((a, b) => b.bw - a.bw); return maxH >= 720 ? s[0] : s[s.length - 1]; }; const pickAudio = (pl, v) => { if (!pl.audio.length) return null; const own = v.aud ? pl.audio.filter(a => a.group === v.aud) : []; if (!own.length && (!v.codecs || /mp4a|ac-3|ec-3|opus|flac/i.test(v.codecs))) return null; const c = own.length ? own : pl.audio; return c.find(a => a.def) || c.find(a => a.auto) || c[0]; }; const DIR = 'bcu_rec'; const defRoot = () => navigator.storage.getDirectory(); class DiskBuffer { constructor(o = {}) { this.id = o.id || `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; this.meta = o.meta || {}; this.chunk = o.chunkBytes || 4 * 1048576; this.getRoot = o.getRoot || defRoot; this.mem = []; this.memBytes = 0; this.files = []; this.size = 0; this.n = 0; this.dir = null; this.base = null; this._q = this._init(); } get onDisk() { return !!this.dir; } async _init() { try { const root = await this.getRoot(); this.base = await root.getDirectoryHandle(DIR, { create: true }); this.dir = await this.base.getDirectoryHandle(this.id, { create: true }); await this._put('meta.json', new Blob([JSON.stringify({ ...this.meta, id: this.id, t: Date.now() })])); } catch { this.dir = null; } } async _put(name, blob) { const fh = await this.dir.getFileHandle(name, { create: true }); const w = await fh.createWritable(); try { await w.write(blob); } finally { await w.close(); } return fh; } async _flush() { if (!this.dir || !this.memBytes) return; const parts = this.mem, bytes = this.memBytes; this.mem = []; this.memBytes = 0; try { const fh = await this._put(`${String(++this.n).padStart(6, '0')}.bin`, new Blob(parts)); this.files.push(fh); } catch { this.mem = parts.concat(this.mem); this.memBytes += bytes; this.dir = null; } } append(data) { const len = data.size ?? data.byteLength; if (!len) return; this.size += len; this.mem.push(data); this.memBytes += len; if (this.memBytes >= this.chunk) this._q = this._q.then(() => this._flush()); } async finish() { await this._q; const f = await Promise.all(this.files.map(h => h.getFile())); return new Blob([...f, ...this.mem], { type: this.meta.mime || '' }); } async markDone() { await this._q; if (this.dir) { try { await this._put('.done', new Blob([])); } catch (e) { dbg(e, 'DiskBuffer.markDone'); } } } async remove() { await this._q.catch(() => {}); try { await this.base?.removeEntry(this.id, { recursive: true }); } catch (e) { dbg(e, 'DiskBuffer.remove'); } this.dir = null; this.files = []; } dispose(delay = 900000) { setTimeout(() => this.remove(), delay); } static async list(getRoot = defRoot, minAge = 120000) { const out = []; try { const base = await (await getRoot()).getDirectoryHandle(DIR); for await (const [id, h] of base.entries()) { if (h.kind !== 'directory') continue; const e = { id, meta: {}, bins: [], done: false, last: 0, bytes: 0 }; for await (const [name, fh] of h.entries()) { const f = await fh.getFile(); e.last = Math.max(e.last, f.lastModified); if (name === '.done') e.done = true; else if (name === 'meta.json') { try { e.meta = JSON.parse(await f.text()); } catch (e) { dbg(e, 'DiskBuffer.list'); } } else if (name.endsWith('.bin')) { e.bins.push([name, f]); e.bytes += f.size; } } e.bins.sort((a, b) => a[0] < b[0] ? -1 : 1); e.stale = Date.now() - e.last >= minAge; out.push(e); } } catch (e) { dbg(e, 'DiskBuffer.list'); } return out; } static async orphans(getRoot, minAge) { return (await DiskBuffer.list(getRoot, minAge)).filter(e => !e.done && e.stale && e.bytes > 0); } static blobOf(e) { return new Blob(e.bins.map(b => b[1]), { type: e.meta.mime || '' }); } static async cleanup(getRoot = defRoot, doneAge = 1800000) { try { const base = await (await getRoot()).getDirectoryHandle(DIR); for (const e of await DiskBuffer.list(getRoot, 0)) { const old = Date.now() - e.last; if ((e.done && old > doneAge) || (!e.bytes && old > 300000)) { try { await base.removeEntry(e.id, { recursive: true }); } catch (e) { dbg(e, 'DiskBuffer.cleanup'); } } } } catch (e) { dbg(e, 'DiskBuffer.cleanup'); } } static async markDoneById(id, getRoot = defRoot) { try { const d = await (await (await getRoot()).getDirectoryHandle(DIR)).getDirectoryHandle(id); const w = await (await d.getFileHandle('.done', { create: true })).createWritable(); await w.close(); } catch (e) { dbg(e, 'DiskBuffer.markDoneById'); } } static async removeById(id, getRoot = defRoot) { try { await (await (await getRoot()).getDirectoryHandle(DIR)).removeEntry(id, { recursive: true }); } catch (e) { dbg(e, 'DiskBuffer.removeById'); } } } const sleepMs = (o, ms) => new Promise(r => { const t = setTimeout(r, ms); o._wake = () => { clearTimeout(t); r(); }; }); const ivBytes = (iv, seq) => { if (!iv) return be(seq, 16); const h = iv.replace(/^0x/i, '').padStart(32, '0'), o = new Uint8Array(16); for (let i = 0; i < 16; i++) o[i] = parseInt(h.substr(i * 2, 2), 16); return o; }; const rangeSlice = (d, r) => d.length === r.n ? d : d.length >= r.o + r.n ? d.subarray(r.o, r.o + r.n) : null; const isFmp4Media = d => d.length > 8 && /^(ftyp|styp|moof|sidx)$/.test(String.fromCharCode(d[4], d[5], d[6], d[7])); class HlsCapture { constructor(o) { this.o = { maxHeight: Infinity, partBytes: 150 * 1048576, totalBytes: 0, maxBacklog: 6, retries: 3, ...o }; this.stats = { segments: 0, missed: 0, gaps: 0, bytes: 0, parts: 0, variant: null }; this.stopped = false; this.error = null; this.sink = null; this.mux = null; this.partNo = 0; this.saved = 0; this.initBytes = null; this.initUri = null; this.keys = new Map(); this.pending = []; } stop() { this.stopped = true; this._wake?.(); } start() { this.done = this._run().catch(e => { this.error = e; }).then(() => Promise.all(this.pending)).then(() => ({ error: this.error, stats: this.stats })); return this.done; } async _bin(url, tries = this.o.retries, range = null) { for (let i = 0; i < tries && !this.stopped; i++) { let d = null; try { d = await this.o.fetchBin(url, range || undefined); } catch (e) { dbg(e, 'HlsCapture._bin'); } if (d && range) d = rangeSlice(d, range); if (d && d.length) return d; if (i < tries - 1) await sleepMs(this, 400 * 2 ** i); } return null; } async _pl(url, tries) { const d = await this._bin(url, tries); return d ? parsePlaylist(new TextDecoder().decode(d), url) : null; } async _newSink() { this.partNo++; this.sink = await this.o.makeSink(this.partNo); this.mux = new Fmp4Mux(); if (this.initBytes) this.sink.append(this.initBytes); } async _closePart(final) { if (!this.sink) return; const sink = this.sink, no = this.partNo; this.sink = null; if (this.mux) for (const c of this.mux.flush()) sink.append(c); this.mux = null; this.saved += sink.size; if (sink.size > (this.initBytes ? this.initBytes.length : 0)) { this.stats.parts++; this.pending.push(Promise.resolve().then(async () => this.o.onPart(await sink.finish(), no, { sink, final: !!final })).catch(e => { this.error ||= e; })); } else { try { await sink.remove?.(); } catch (e) { dbg(e, 'HlsCapture._closePart'); } } } async _rotate() { await this._closePart(false); if (!this.stopped) await this._newSink(); } _progress() { this.stats.part = this.sink ? this.sink.size : 0; this.stats.saved = this.saved; this.stats.bytes = this.saved + this.stats.part; this.o.onProgress?.(this.stats); } async _decrypt(data, seg) { if (seg.key.method !== 'AES-128') throw new Error(`unsupported encryption ${seg.key.method}`); let k = this.keys.get(seg.key.uri); if (!k) { k = await this._bin(seg.key.uri); if (!k || k.length !== 16) throw new Error('cannot get key'); this.keys.set(seg.key.uri, k); } const ck = await crypto.subtle.importKey('raw', k, 'AES-CBC', false, ['decrypt']); return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivBytes(seg.key.iv, seg.seq) }, ck, data)); } async _appendTs(data, seg) { const parsed = demuxTs(data); let out; try { out = this.mux.push(parsed, seg.disc); } catch (e) { if (!(e instanceof NeedNewInit)) throw e; await this._rotate(); out = this.mux.push(parsed, true); } for (const c of out) this.sink.append(c); } async _loadInit(map) { const uri = map.key, init = await this._bin(map.uri, undefined, map.range); if (!init) throw new Error('cannot get init segment'); const changed = this.initUri !== null; this.initUri = uri; this.initBytes = init; if (changed && this.sink.size > 0) await this._rotate(); else if (this.sink.size === 0) this.sink.append(init); } async _appendFmp4(data, seg) { if (seg.map && seg.map.key !== this.initUri) await this._loadInit(seg.map); this.sink.append(data); } async _segment(seg) { let data = await this._bin(seg.uri, undefined, seg.range); if (!data) { this.stats.missed++; return false; } if (seg.key) data = await this._decrypt(data, seg); if (data[0] === 0x47) await this._appendTs(data, seg); else if (isFmp4Media(data)) await this._appendFmp4(data, seg); else throw new Error('unsupported segment format'); this.stats.segments++; return true; } async _openHinted() { const h = this.o.hint; if (!h || !Array.isArray(h.variants) || !h.variants.length) return null; const v = pickVariant(h.variants, this.o.maxHeight); const pl = await this._pl(v.url, 1); if (!pl || pl.master || !pl.segs.length) return null; this.stats.variant = v; this.stats.variants = h.variants; const au = pickAudio({ audio: h.audio || [] }, v); this.stats.audioUrl = au ? au.url : ''; if (au) this.o.onAudio?.(au); this._refreshLadder(); return { url: v.url, pl }; } _refreshLadder() { this._pl(this.o.url, 1).then(m => { if (!m || !m.master || this.stopped) return; this.stats.variants = m.variants; this.o.onOpened?.({ variants: m.variants, audio: m.audio }); }).catch(() => {}); } async _openPlaylist() { const o = this.o; const quick = await this._openHinted(); if (quick) return quick; let url = o.url, pl = await this._pl(url, o.hint ? 1 : undefined); if (!pl) throw new Error('playlist unavailable'); if (pl.master) { const masterAudio = pl.audio; const v = pickVariant(pl.variants, o.maxHeight); this.stats.variant = v; this.stats.variants = pl.variants; url = v.url; const au = pickAudio(pl, v); this.stats.audioUrl = au ? au.url : ''; if (au) this.o.onAudio?.(au); pl = await this._pl(url); if (!pl) throw new Error('media playlist unavailable'); this.o.onOpened?.({ variants: this.stats.variants, audio: masterAudio }); } return { url, pl }; } _freshSegs(pl, last) { let fresh = pl.segs.filter(s => s.seq > last); if (last < 0 && !pl.end && fresh.length > this.o.maxBacklog) fresh = fresh.slice(-this.o.maxBacklog); if (last >= 0 && fresh.length && fresh[0].seq > last + 1) { this.stats.gaps += fresh[0].seq - last - 1; fresh[0] = { ...fresh[0], disc: true }; } return fresh; } async _waitGate() { const o = this.o; if (!o.gate || !o.gate()) return false; while (o.gate() && !this.stopped) await sleepMs(this, 400); return true; } async _consume(fresh, st) { const o = this.o; for (const seg of fresh) { if (this.stopped) break; if (await this._waitGate()) return true; if (await this._segment(seg)) st.idleSince = Date.now(); st.last = seg.seq; this._progress(); if (this.sink.size >= o.partBytes) await this._rotate(); if (o.totalBytes && this.saved + this.sink.size >= o.totalBytes) this.stopped = true; } return false; } async _refreshPl(url, pl) { return (await this._pl(url, 1).catch(() => null)) || pl; } _shouldStop(pl, fresh, idleSince) { return this.stopped || pl.end || (!fresh.length && Date.now() - idleSince > Math.max(20000, pl.target * 4000)); } async _run() { let { url, pl } = await this._openPlaylist(); await this._newSink(); const st = { last: -1, idleSince: Date.now(), fails: 0 }; while (!this.stopped) { const fresh = this._freshSegs(pl, st.last); const held = await this._consume(fresh, st); if (held && !this.stopped) { pl = await this._refreshPl(url, pl); continue; } if (this._shouldStop(pl, fresh, st.idleSince)) break; await sleepMs(this, Math.max(500, pl.target * 500)); if (this.stopped) break; const next = await this._pl(url, 1).catch(() => null); if (next) { pl = next; st.fails = 0; } else if (++st.fails > 12) break; } await this._closePart(true); } } const boxAt = (b, tag) => { const c0 = tag.charCodeAt(0), c1 = tag.charCodeAt(1), c2 = tag.charCodeAt(2), c3 = tag.charCodeAt(3); for (let i = 4; i + 4 <= b.length; i++) if (b[i] === c0 && b[i + 1] === c1 && b[i + 2] === c2 && b[i + 3] === c3) return i + 4; return -1; }; const aotOf = (b, e) => { let i = e + 4; const len = () => { let n = 0, c; do { c = b[i++]; n = (n << 7) | (c & 127); } while ((c & 128) && i < b.length); return n; }; try { if (b[i++] !== 3) return 2; len(); const fl = b[i + 2]; i += 3; if (fl & 0x80) i += 2; if (fl & 0x40) i += 1 + b[i]; if (fl & 0x20) i += 2; if (b[i++] !== 4) return 2; len(); i += 13; if (b[i++] !== 5) return 2; len(); const aot = b[i] >> 3; return aot === 31 || !aot ? 2 : aot; } catch { return 2; } }; const codecsOf = b => { const hex = n => n.toString(16).padStart(2, '0'); const v = boxAt(b, 'avcC'), h = v < 0 ? boxAt(b, 'hvcC') : -1; if (v < 0 && h < 0) return null; const out = [v >= 0 ? `avc1.${hex(b[v + 1])}${hex(b[v + 2])}${hex(b[v + 3])}` : hevcCodecString({ space: b[h + 1] >> 6, tier: (b[h + 1] >> 5) & 1, prof: b[h + 1] & 31, compat: [...b.subarray(h + 2, h + 6)], cons: [...b.subarray(h + 6, h + 12)], level: b[h + 12] })]; if (boxAt(b, 'mp4a') >= 0) { const e = boxAt(b, 'esds'); out.push(`mp4a.40.${e >= 0 ? aotOf(b, e) : 2}`); } return out.join(','); }; const audioCodecOf = b => { if (boxAt(b, 'mp4a') >= 0) { const e = boxAt(b, 'esds'); return `mp4a.40.${e >= 0 ? aotOf(b, e) : 2}`; } if (boxAt(b, 'Opus') >= 0) return 'opus'; if (boxAt(b, 'ac-3') >= 0) return 'ac-3'; if (boxAt(b, 'ec-3') >= 0) return 'ec-3'; return null; }; const timescaleOf = b => { const i = boxAt(b, 'mdhd'); if (i < 0) return 0; const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); return b[i] === 1 ? dv.getUint32(i + 20) : dv.getUint32(i + 12); }; const tfdtSec = (b, ts) => { const i = boxAt(b.subarray(0, Math.min(b.length, 1024)), 'tfdt'); if (i < 0 || !ts) return null; const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); const t = b[i] === 1 ? dv.getUint32(i + 4) * TWO32 + dv.getUint32(i + 8) : dv.getUint32(i + 4); return t / ts; }; const isInit = d => d.length > 8 && d[4] === 0x66 && d[5] === 0x74 && d[6] === 0x79 && d[7] === 0x70; const bufferedHas = (b, t) => { for (let i = 0; i < b.length; i++) if (t >= b.start(i) - 0.05 && t <= b.end(i)) return true; return false; }; class HlsPlayer { constructor(o) { this.o = { maxHeight: Infinity, maxBacklog: 2, keep: 30, lag: 10, ...o }; this.v = this.o.video; this.ms = null; this.sb = null; this.src = ''; this.q = []; this.codec = ''; this.ts = 90000; this.align = false; this.dead = false; this.started = false; this.cap = null; this.timer = 0; this.error = null; this.shift = false; this.full = false; this.asb = null; this.aq = []; this.acap = null; this.aCodec = ''; this.wantAudio = false; this.aDead = false; this.t0 = Date.now(); } get stats() { return this.cap ? this.cap.stats : {}; } async start() { if (typeof MediaSource === 'undefined') throw new Error('MediaSource is not supported'); const ms = this.ms = new MediaSource(); this.src = URL.createObjectURL(ms); this.v.src = this.src; await new Promise((res, rej) => { ms.addEventListener('sourceopen', res, { once: true }); setTimeout(() => rej(new Error('sourceopen timeout')), 10000); }); if (this.dead) return { error: null, stats: {} }; this._onErr = () => this.fail(new Error('video error')); this.v.addEventListener('error', this._onErr); this._onPause = () => { if (!this.dead && this.started) { this.o.hold = true; this.shift = true; } }; this._onPlay = () => { this.o.hold = false; if (this.full) { this.full = false; this._pump(); } }; this.v.addEventListener('pause', this._onPause); this.v.addEventListener('play', this._onPlay); this.timer = setInterval(() => this._tick(), 1000); this.cap = new HlsCapture({ url: this.o.url, fetchBin: this.o.fetchBin, maxHeight: this.o.maxHeight, maxBacklog: this.o.maxBacklog, gate: () => this._gate(), hint: this.o.hint, onOpened: this.o.onOpened, partBytes: Infinity, totalBytes: 0, onAudio: au => this._startAudio(au), makeSink: () => this._sink(), onPart: async () => {} }); const res = await this.cap.start(); this.acap?.stop(); const out = { error: this.error || res.error, stats: res.stats }; if (!this.dead) this.o.onEnd?.(out); return out; } _sink() { const p = this; return { size: 0, append(d) { this.size += d.byteLength; p._enqueue(d); }, finish: async () => new Blob([]), remove: async () => {} }; } _enqueue(d) { if (this.dead) return; this.q.push({ init: isInit(d), data: d }); this._pump(); } _startAudio(au) { if (this.dead || this.acap) return; this.wantAudio = true; this.acap = new HlsCapture({ url: au.url, fetchBin: this.o.fetchBin, maxBacklog: this.o.maxBacklog, gate: () => this._aGate(), partBytes: Infinity, totalBytes: 0, makeSink: () => this._aSink(), onPart: async () => {} }); this.acap.start().then(r => { if (!this.dead && r && r.error) this._dropAudio(r.error); }).catch(e => this._dropAudio(e)); } _aSink() { const p = this; return { size: 0, append(d) { this.size += d.byteLength; p._aEnqueue(d); }, finish: async () => new Blob([]), remove: async () => {} }; } _aEnqueue(d) { if (this.dead || this.aDead) return; this.aq.push({ init: isInit(d), data: d }); this._pump(); this._aPump(); } _aGate() { return this.full || this.aq.length > 6 || (!!this.o.hold && this._ahead() > 90); } _aMime(data) { const codec = audioCodecOf(data); if (!codec) throw new Error('unsupported audio codec'); const mime = `audio/mp4; codecs="${codec}"`; if (!MediaSource.isTypeSupported(mime)) throw new Error(`unsupported audio ${codec}`); return { codec, mime }; } _aCreate(data) { const { codec, mime } = this._aMime(data); const sb = this.asb = this.ms.addSourceBuffer(mime); sb.addEventListener('updateend', () => { this._after(); this._aPump(); }); sb.addEventListener('error', () => this._dropAudio(new Error('audio append error'))); this.aCodec = codec; } _aInit(data) { if (!this.asb) { this._aCreate(data); return; } const { codec, mime } = this._aMime(data); if (codec !== this.aCodec) { this.asb.changeType(mime); this.aCodec = codec; } } _audioReady() { if (!this.wantAudio || this.aDead || this.asb) return true; const a = this.aq[0]; if (!a || !a.init) return false; try { this._aCreate(a.data); queueMicrotask(() => this._aPump()); } catch (e) { this._dropAudio(e); } return true; } _aOpen() { return !this.dead && !this.aDead && !!this.ms && this.ms.readyState === 'open'; } _aCanPump() { return this._aOpen() && this.aq.length > 0 && !(this.asb && this.asb.updating); } _aPrepare(op) { if (op.init) { this._aInit(op.data); return true; } if (!this.asb) return false; if (this.sb) this.asb.timestampOffset = this.sb.timestampOffset; return true; } _aPump() { if (!this._aCanPump() || !this.asb) return; const op = this.aq[0]; try { const ok = this._aPrepare(op); this.aq.shift(); if (ok) this.asb.appendBuffer(op.data); } catch (e) { this._aErr(e, op); } } _aErr(e, op) { if (!e || e.name !== 'QuotaExceededError') { this._dropAudio(e); return; } if (this.aq[0] !== op) this.aq.unshift(op); if (!this._trim(true) && !(this.v.paused || this.o.hold)) this._dropAudio(e); } _dropAudio(e) { if (this.aDead) return; this.aDead = true; this.o.onAudioFail?.(e); try { this.acap?.stop(); } catch (e) { dbg(e, 'HlsPlayer._dropAudio'); } this.aq.length = 0; if (this.asb) { try { this.asb.abort(); } catch (e) { dbg(e, 'HlsPlayer._dropAudio'); } try { this.ms.removeSourceBuffer(this.asb); } catch (e) { dbg(e, 'HlsPlayer._dropAudio'); } this.asb = null; } this._after(); this._pump(); } _ahead() { const b = this.v.buffered; return b.length ? b.end(b.length - 1) - this.v.currentTime : 0; } _gate() { return this.full || this.q.length > 6 || (!!this.o.hold && this._ahead() > 90); } _end() { const b = this.sb && this.sb.buffered; return b && b.length ? b.end(b.length - 1) : 0; } _canPump() { const ms = this.ms; if (this.dead || !ms || ms.readyState !== 'open' || !this.q.length) return false; return !(this.sb && this.sb.updating); } _createBuffer(mime, codec) { if (!MediaSource.isTypeSupported(mime)) throw new Error(`unsupported codec ${codec}`); const sb = this.sb = this.ms.addSourceBuffer(mime); sb.addEventListener('updateend', () => { this._after(); this._pump(); }); sb.addEventListener('error', () => this.fail(new Error('append error'))); this.codec = codec; } _applyInit(data) { const codec = codecsOf(data); if (!codec) throw new Error('unsupported codec'); const mime = `video/mp4; codecs="${codec}"`; if (!this.sb) this._createBuffer(mime, codec); else { if (codec !== this.codec) { this.sb.changeType(mime); this.codec = codec; } this.align = true; } this.ts = timescaleOf(data) || 90000; } _alignOffset(data) { this.align = false; const t0 = tfdtSec(data, this.ts), end = this._end(); this.sb.timestampOffset = (t0 !== null && t0 + 0.5 < end) ? end - t0 : 0; } _onAppendError(e, op) { if (!e || e.name !== 'QuotaExceededError') { this.fail(e); return; } if (this.q[0] !== op) this.q.unshift(op); if (this._trim(true)) return; if (this.v.paused || this.o.hold) this.full = true; else this.fail(e); } _pump() { if (!this._canPump()) return; const op = this.q[0]; if (op.init && !this.sb && !this._audioReady()) return; try { if (op.init) this._applyInit(op.data); else if (this.align) this._alignOffset(op.data); this.q.shift(); this.sb.appendBuffer(op.data); } catch (e) { this._onAppendError(e, op); } } _trimAudio(cut) { const a = this.asb; if (!a || a.updating || !a.buffered.length || cut <= a.buffered.start(0)) return; try { a.remove(a.buffered.start(0), cut); } catch (e) { dbg(e, 'HlsPlayer._trimAudio'); } } _trim(force) { const sb = this.sb; if (!sb || sb.updating || !sb.buffered.length) return false; const s = sb.buffered.start(0), cut = this.v.currentTime - (force ? 2 : this.o.keep); if (cut - s <= 1) return false; this._trimAudio(cut); try { sb.remove(s, cut); return true; } catch { return false; } } _after() { const v = this.v; if (!this.started && v.buffered.length && !(this.wantAudio && !this.asb && !this.aDead)) { this.started = true; this.tStart = Date.now(); try { v.currentTime = v.buffered.start(0) + 0.001; } catch (e) { dbg(e, 'HlsPlayer._after'); } v.play().catch(() => { if (!v.muted) { v.muted = true; this.o.onMuted?.(); v.play().catch(() => {}); } }); } } _seekIntoBuffer(b) { const v = this.v, cur = v.currentTime; for (let i = 0; i < b.length; i++) if (b.start(i) > cur) { v.currentTime = b.start(i) + 0.001; return; } if (cur < b.start(0)) v.currentTime = b.start(0) + 0.001; } _chaseLive(b) { const v = this.v, cur = v.currentTime, end = b.end(b.length - 1); if (end - cur > this.o.lag && !this.shift) v.currentTime = end - 1.5; else if (v.paused && !v.ended && !this.o.hold) v.play().catch(() => {}); } _audioTick() { if (!this.wantAudio || this.aDead) return; this._pump(); this._aPump(); if (!this.started && Date.now() - this.t0 > 15000) this._dropAudio(new Error('audio timeout')); } _diag() { const r = b => { try { return b && b.length ? `${b.start(0).toFixed(1)}-${b.end(b.length - 1).toFixed(1)}` : '-'; } catch { return '?'; } }; const v = this.v; return `no video v${this.sb ? r(this.sb.buffered) : 'x'} a${this.asb ? r(this.asb.buffered) : 'x'} s${v.readyState} t${v.currentTime.toFixed(1)}${this.aDead ? ' ad' : ''}`; } _videoWatch() { if (!this.started || this.error || this.v.videoWidth || Date.now() - this.tStart < 8000) return; this.fail(new Error(this._diag())); } _syncPlayback() { const v = this.v, b = v.buffered; if (!this.started || !b.length) return; if (bufferedHas(b, v.currentTime)) this._chaseLive(b); else this._seekIntoBuffer(b); } _tick() { if (this.dead) return; this._audioTick(); const v = this.v, sb = this.sb; if (!sb) return; if (!sb.updating) this._trim(false); if (this.full && !v.paused) { this.full = false; this._pump(); } this._videoWatch(); this._syncPlayback(); } fail(e) { if (this.dead) return; this.error = this.error || e; this.o.onError?.(e); this.cap?.stop(); this.acap?.stop(); } stop() { if (this.dead) return; this.dead = true; this.cap?.stop(); try { this.acap?.stop(); } catch (e) { dbg(e, 'HlsPlayer.stop'); } clearInterval(this.timer); this.q.length = 0; this.aq.length = 0; try { this.v.removeEventListener('error', this._onErr); this.v.removeEventListener('pause', this._onPause); this.v.removeEventListener('play', this._onPlay); } catch (e) { dbg(e, 'HlsPlayer.stop'); } try { this.v.pause(); this.v.removeAttribute('src'); this.v.load(); } catch (e) { dbg(e, 'HlsPlayer.stop'); } if (this.src) { try { URL.revokeObjectURL(this.src); } catch (e) { dbg(e, 'HlsPlayer.stop'); } } } } return { Fmp4Mux, NeedNewInit, demuxTs, parseSps, parseHevcSps, hevcCodecString, codecsOf, parsePlaylist, pickVariant, DiskBuffer, HlsCapture, HlsPlayer, buildInit }; })(); const REC_MIME_MP4 = ['video/mp4;codecs=avc1,mp4a.40.2', 'video/mp4']; const REC_MIME_WEBM = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm']; const recMimeFor = fmt => { const order = fmt === 'webm' ? [...REC_MIME_WEBM, ...REC_MIME_MP4] : [...REC_MIME_MP4, ...REC_MIME_WEBM]; return order.find(t => MediaRecorder.isTypeSupported(t)) || 'video/webm'; }; const recFmtText = () => REC_FMT_T[AppState.lang] || REC_FMT_T.EN; class MediaRecorderManager { constructor(videoController, uiController) { this.vc = videoController; this.ui = uiController; this.mRec = null; this.disk = null; this.interval = null; this.seconds = 0; this.totalSize = 0; this.savedBytes = 0; this.cfg = recCfg(); this.partNum = 1; this.partStart = 0; this.recStartTime = ''; this.splitRequested = false; this.wakeLock = null; this.currentStream = null; this._onTrackEnded = () => this.stopActive(); onPageEvent('visibilitychange', () => { if (!realHidden() && this.mRec && this.mRec.state === 'recording') this.acquireWakeLock(); }); setTimeout(async () => { try { await HlsKit.DiskBuffer.cleanup(); const n = (await HlsKit.DiskBuffer.orphans()).length; const b = this.ui?.elements?.bRec; if (n && b) this.ui.flash(b, `↻ ${n}`); } catch (e) { dbg(e, 'MediaRecorderManager'); } }, 6000); window.addEventListener('beforeunload', e => { if (this.mRec && this.mRec.state === 'recording') { e.preventDefault(); e.returnValue = ''; } }); } getStream(v) { return v.captureStream?.(30) ?? v.mozCaptureStream?.(30) ?? null; } acquireWakeLock() { if (!('wakeLock' in navigator) || this.wakeLock || this._wlPending) return; this._wlPending = true; navigator.wakeLock.request('screen').then(wl => { this._wlPending = false; if (!(this.mRec && this.mRec.state !== 'inactive')) { wl.release().catch(() => {}); return; } this.wakeLock = wl; wl.addEventListener('release', () => { if (this.wakeLock === wl) this.wakeLock = null; }); }).catch(() => { this._wlPending = false; }); } stopActive() { if (this.mRec && this.mRec.state !== 'inactive') { this.splitRequested = false; try { this.mRec.stop(); } catch (e) { dbg(e, 'MediaRecorderManager.stopActive'); } } } cleanupSession(btn, sub) { clearInterval(this.interval); btn.classList.remove('on', 'rec-active'); if (sub) sub.textContent = L().rec; if (this.wakeLock) { this.wakeLock.release().catch(() => {}); this.wakeLock = null; } try { this.currentStream?.getTracks().forEach(t => t.stop()); } catch (e) { dbg(e, 'MediaRecorderManager.cleanupSession'); } } saveBlob(blob, ext, durationMs, name, disk) { return saveRecording(blob, ext, durationMs, name, disk); } async recoverOrphans() { let n = 0; for (const e of await HlsKit.DiskBuffer.orphans(undefined, 0)) { const ext = e.meta.ext || 'webm'; await this.saveBlob(HlsKit.DiskBuffer.blobOf(e), ext, 0, `${e.meta.name || 'REC'}_recovered_${e.id}.${ext}`, null); await HlsKit.DiskBuffer.markDoneById(e.id); n++; await new Promise(r => setTimeout(r, 800)); } return n; } async fillRecovery(box) { try { const n = (await HlsKit.DiskBuffer.orphans(undefined, 0)).length; if (!n || !box.isConnected) return; const b = mk('button', 'bm-tbtn'); b.textContent = `${L().rs_recover} (${n})`; b.onclick = async () => { b.disabled = true; await this.recoverOrphans(); b.remove(); }; box.append(b); } catch (e) { dbg(e, 'MediaRecorderManager.fillRecovery'); } } hlsFetch(url, range) { return gmBytes(url, null, false, range); } startHls(btn, sub) { const url = getStreamUrl(); if (!url) throw new Error('no hls url'); const cfg = this.cfg = recCfg(); const base = `${currentSite.getName() || 'REC'}_${this.recStartTime = Helpers.getTimestamp()}`; this.partNum = 1; this.seconds = 0; this.savedBytes = 0; this.totalSize = 0; this.currentStream = null; let userStop = false; const cap = newHlsCapture({ url, cfg, base, fetchBin: (u, r) => this.hlsFetch(u, r), onProgress: st => { this.totalSize = st.part; this.savedBytes = st.saved; }, save: (blob, name, sink) => this.saveBlob(blob, 'mp4', 0, name, sink) }); const shim = this.mRec = { state: 'recording', stop: () => { userStop = true; shim.state = 'inactive'; cap.stop(); } }; this.hls = cap; btn.classList.add('on', 'rec-active'); const label = () => `${Helpers.formatTime(this.seconds)} | ${fmtShort(this.savedBytes + this.totalSize)}`; if (sub) sub.textContent = label(); clearInterval(this.interval); const t0 = Date.now(); this.interval = setInterval(() => { this.seconds = Math.floor((Date.now() - t0) / 1000); if (sub) sub.textContent = label(); }, 1000); this.acquireWakeLock(); cap.start().then(res => { shim.state = 'inactive'; if (this.hls !== cap || this.mRec !== shim) return; const v = this.vc.cachedVideo; if (res.error && !userStop && !res.stats.segments && v) { this.ui.flash(btn, '! HLS'); try { this.start(v, btn, sub); return; } catch (e) { dbg(e, 'MediaRecorderManager.startHls'); } } if (res.error && !userStop) this.ui.flash(btn, `! ${L().err}`); this.cleanupSession(btn, sub); }); } updateInfo() { const el = this._rs?.info; if (!el || !el.isConnected) return; const l = L(), on = this.mRec && this.mRec.state !== 'inactive'; el.textContent = on ? `${l.rs_now}: ${l.rs_p} ${fmtBytes(this.totalSize)} · ${l.rs_t} ${fmtBytes(this.savedBytes + this.totalSize)}` : ''; } buildRsShell() { const { overlay, modal, title, closeBtn, body } = buildModalShell(this.ui.shadow, 'bm-modal rs-modal rs-pult'); modal.style.setProperty('background', 'var(--pb)', 'important'); closeBtn.innerHTML = ICON_CLOSE; const rsCss = document.createElement('style'); rsCss.textContent = RS_PULT_CSS; overlay.appendChild(rsCss); const close = () => { overlay.classList.remove('open'); clearInterval(this._rsTimer); overlay._fromSt = false; overlay._bk?.(); }; closeBtn.onclick = close; overlay.onclick = e => { if (e.target === overlay && Date.now() - (this._rsAt || 0) > 400) close(); }; this._rs = { overlay, title, body, close, info: null }; } renderRecSettings() { const { title, body } = this._rs; const l = L(), cfg = this.cfg = recCfg(); title.innerHTML = `<span class="rs-ic">${icons.rec}</span><span>${escHTML(l.rs_title)}</span>`; const set = patch => { Store.set('bc_rec', { ...recCfg(), ...patch }); this.renderRecSettings(); }; const chip = (txt, on, fn) => { const b = mk('button', `bm-tbtn${on ? ' on' : ''}`); b.textContent = txt; b.onclick = fn; return b; }; const size = v => v >= 1024 ? `${v / 1024} ${l.u_gb}` : `${v} ${l.u_mb}`; const qs = mk('div', 'rs-qs'); Object.keys(REC_Q).forEach(q => { const mb = (REC_Q[q][0] + REC_Q[q][1]) / 8 * 60 / 1048576; const b = mk('button', `bm-btn${cfg.q === q ? ' sel' : ''}`); const r1 = mk('b'); r1.textContent = REC_RES[q]; const r2 = mk('span'); r2.textContent = l['rs_q_' + q]; const r3 = mk('small'); r3.textContent = `${l.rs_upto} ${Math.round(mb)} ${l.rs_mbmin} · ${(mb * 60 / 1024).toFixed(1)} ${l.rs_gbh}`; b.append(r1, r2, r3); b.onclick = () => set({ q }); qs.append(b); }); const fmts = mk('div', 'rs-chips'); [['mp4', 'MP4'], ['webm', 'WebM']].forEach(([v, n]) => fmts.append(chip(n, cfg.fmt === v, () => set({ fmt: v })))); const fmtNote = cfg.fmt === 'mp4' && typeof MediaRecorder !== 'undefined' && !REC_MIME_MP4.some(t => MediaRecorder.isTypeSupported(t)) ? textEl('rs-note', recFmtText()[1]) : null; const srcs = mk('div', 'rs-chips rs-src'); [['cap', l.rs_src_cap], ['hls', l.rs_src_hls]].forEach(([v, n]) => srcs.append(chip(n, cfg.src === v, () => set({ src: v })))); const recov = mk('div', 'rs-chips'); this.fillRecovery(recov); const parts = mk('div', 'rs-chips'); REC_PART.forEach(v => parts.append(chip(size(v), cfg.part === v, () => set({ part: v })))); const totals = mk('div', 'rs-chips'); REC_TOTAL.forEach(v => totals.append(chip(v ? size(v) : l.rs_off, cfg.total === v, () => set({ total: v })))); const info = mk('div', 'rs-info'); this._rs.info = info; const vid = (typeof largestVideo === 'function' && largestVideo()) || null; const res = vid && vid.videoHeight ? `${vid.videoWidth}×${vid.videoHeight}` : '—'; const resEl = textEl('rs-note', `${l.rs_cur} ${res}`); const resNote = textEl('rs-note', l.rs_resnote); const sum = mk('div', 'rs-sum'); sum.innerHTML = icons.rec; const sumTx = mk('span'); sumTx.textContent = [REC_RES[cfg.q], cfg.fmt === 'mp4' ? 'MP4' : 'WebM', size(cfg.part), cfg.total ? size(cfg.total) : l.rs_off].join(' · '); sum.appendChild(sumTx); const grp = (...kids) => { const g = mk('div', 'rs-g'); g.append(...kids); return g; }; body.replaceChildren( sum, grp(textEl('hd-sec', l.rs_quality), qs), grp(textEl('hd-sec', l.rs_src), srcs, textEl('rs-note', l.rs_src_note), recov), grp(textEl('hd-sec', recFmtText()[0]), fmts, ...(fmtNote ? [fmtNote] : []), textEl('hd-sec', l.rs_part), parts, textEl('hd-sec', l.rs_total), totals), info, resEl, resNote, textEl('rs-note', l.rs_hint), textEl('rs-note', l.rs_next) ); this.updateInfo(); } openSettings() { if (!this._rs) this.buildRsShell(); const { overlay } = this._rs; this.renderRecSettings(); this._rsAt = Date.now(); overlay.classList.add('open'); clearInterval(this._rsTimer); this._rsTimer = setInterval(() => { if (!overlay.classList.contains('open')) return clearInterval(this._rsTimer); this.updateInfo(); }, 1000); } toggle() { const v = this.vc.cachedVideo; const btn = this.ui.elements.bRec; const sub = btn.querySelector('sub'); if (this.mRec && this.mRec.state !== 'inactive') return this.stop(btn, sub); if (recCfg().src === 'hls' && getStreamUrl()) { try { return this.startHls(btn, sub); } catch (e) { dbg(e, 'MediaRecorderManager.toggle'); } } if (!v) return this.ui.flash(btn, `! ${L().no_vid}`); try { this.start(v, btn, sub); } catch { this.ui.flash(btn, `! ${L().err}`); } } stop(btn, sub) { this.splitRequested = false; try { this.mRec.stop(); } catch { this.currentStream?.getTracks().forEach(t => t.stop()); } this.cleanupSession(btn, sub); } start(v, btn, sub) { const s = this.getStream(v); if (!s) throw new Error('no stream'); this.currentStream = s; if (!s.getAudioTracks().length) this.ui.flash(btn, '! AUD'); const mt = recMimeFor(recCfg().fmt); this.recStartTime = Helpers.getTimestamp(); this.partNum = 1; this.seconds = 0; this.savedBytes = 0; this.cfg = recCfg(); this.startNewRecord(s, mt, mt.includes('mp4') ? 'mp4' : 'webm', btn, sub); btn.classList.add('on', 'rec-active'); const label = () => `${Helpers.formatTime(this.seconds)} | ${fmtShort(this.savedBytes + this.totalSize)}`; if (sub) sub.textContent = label(); clearInterval(this.interval); const t0 = Date.now(); this.interval = setInterval(() => { this.seconds = Math.floor((Date.now() - t0) / 1000); if (sub) sub.textContent = label(); }, 1000); } initMediaRecorder(stream, mimeType) { this.totalSize = 0; this.mRec = null; this.mRec = newMediaRecorder(stream, mimeType, this.cfg.q); if (!this.mRec) throw new Error('MediaRecorder init failed'); } bindRecorderEvents(stream, mimeType, ext, btn, sub) { const stopRec = () => { try { this.mRec.stop(); } catch (e) { dbg(e, 'bindRecorderEvents.stopRec'); } }; const savePart = (disk, dur) => { const suffix = (this.partNum > 1 || this.splitRequested) ? `_PT${this.partNum}` : ''; const name = `${currentSite.getName() || 'REC'}_${this.recStartTime}${suffix}.${ext}`; disk.finish().then(blob => this.saveBlob(blob, ext, dur, name, disk)).catch(() => {}); }; const continueSplit = () => { this.partNum++; this.splitRequested = false; const v = this.vc.cachedVideo; const nextStream = (!stream.active && v ? this.getStream(v) : null) || stream; this.currentStream = nextStream; try { this.startNewRecord(nextStream, mimeType, ext, btn, sub); return true; } catch { return false; } }; this.mRec.ondataavailable = e => { if (!(e.data?.size > 0)) return; this.disk.append(e.data); this.totalSize += e.data.size; const limTotal = this.cfg.total * 1048576; if (limTotal && this.savedBytes + this.totalSize >= limTotal) { this.splitRequested = false; return stopRec(); } if (this.totalSize >= this.cfg.part * 1048576 && !this.splitRequested) { this.splitRequested = true; stopRec(); } }; this.mRec.onerror = () => this.stopActive(); this.mRec.onstop = () => { const disk = this.disk; this.disk = null; const dur = performance.now() - this.partStart; if (disk && disk.size > 0) savePart(disk, dur); else disk?.remove(); this.savedBytes += this.totalSize; if (this.splitRequested && continueSplit()) return; this.cleanupSession(btn, sub); }; } startNewRecord(stream, mimeType, ext, btn, sub) { this.initMediaRecorder(stream, mimeType); this.disk = new HlsKit.DiskBuffer({ meta: { name: `${currentSite.getName() || 'REC'}_${this.recStartTime}_PT${this.partNum}`, ext, mime: mimeType.split(';')[0] } }); stream.getTracks().forEach(t => t.addEventListener('ended', this._onTrackEnded)); this.bindRecorderEvents(stream, mimeType, ext, btn, sub); try { this.partStart = performance.now(); this.mRec.start(1000); this.acquireWakeLock(); } catch (err) { this.cleanupSession(btn, sub); this.ui.flash(btn, `! ${L().err}`); throw err; } } } const MV_MAX = 100; const MV_CSS = `.fav-modal .mv-pane{display:none} .bm-overlay.pipkeep:not(.open){opacity:.02!important} .fav-modal:not(.mode-mv) .mv-pane.keep{display:flex!important;position:fixed!important;left:0!important;top:0!important;width:100%!important;height:auto!important;opacity:.02!important;pointer-events:none!important;z-index:0!important} .bm-overlay.pipkeep:not(.open) .fav-modal:not(.mode-mv) .mv-pane.keep{opacity:1!important} .fav-modal.mode-mv .mv-pane{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;gap:10px;padding:12px 14px} .fav-modal.mode-mv .bm-search-wrap,.fav-modal.mode-mv .bm-tags-wrap,.fav-modal.mode-mv .bm-list,.fav-modal.mode-mv .bm-foot,.fav-modal.mode-mv .bm-tools,.fav-modal.mode-mv .bm-setp,.fav-modal.mode-mv .hs-pane,.fav-modal.mode-mv .cal-pane{display:none!important} .mv-bar{display:flex;flex:none;flex-wrap:wrap;align-items:center;gap:8px} .mv-bar .bm-tbtn{display:inline-flex;align-items:center;justify-content:center;gap:6px} .mv-bar .bm-tbtn svg{display:block;flex:none;margin:0} .mv-bar{flex-wrap:nowrap} .mv-bar>*{flex:1 1 0!important;min-width:0!important;margin:0!important} .mv-bar>:first-child{flex:2.4 1 0!important} .mv-bar>:not(:first-child){padding:0!important} .mv-bar>.bm-hbtn{width:auto!important;height:36px;display:inline-flex;align-items:center;justify-content:center} .mv-bar>.bm-hbtn svg{width:18px!important;height:18px!important} .mv-bar>.bm-hbtn.on{background:var(--th);border-color:transparent} .mv-bar>.bm-gear{border:1px solid var(--th)!important} .mv-bar>.bm-gear.on{border-color:transparent!important} .mv-bar>.mv-clr{color:#e5707e!important;border-color:rgba(229,112,126,.45)!important} .mv-stage{position:relative;flex:1 1 auto;min-height:0;display:flex} .mv-grid{flex:1;display:grid;gap:8px;align-content:start;min-width:0;min-height:0;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .mv-tile{position:relative;display:flex;flex-direction:column;min-width:0;background:#000;border-radius:12px;overflow:hidden;outline:2px solid transparent;outline-offset:-2px;cursor:pointer} .mv-tile.act{outline-color:var(--th,#3b82f6)} .mv-tile video{width:100%;height:100%;object-fit:contain;display:block;background:#000;pointer-events:none} .mv-stg{position:relative;flex:none;aspect-ratio:16/9;min-width:0;background:#000} .mv-nm{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis} .mv-vw{flex:none;display:inline-flex;align-items:center;gap:4px;margin-inline-start:auto} .mv-vw svg{width:12px;height:12px} .mv-lbl{display:flex;align-items:center;gap:8px;flex:none;padding:5px 10px;background:rgba(255,255,255,.08);color:#fff;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none} .mv-tb{position:static;flex:none;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;padding:6px 8px;background:rgba(255,255,255,.04);cursor:default} .mv-tb button{width:100%;min-width:0;height:34px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:8px;background:rgba(0,0,0,.65);color:#fff;cursor:pointer} .mv-tb button:hover{background:rgba(0,0,0,.85)} .mv-msg{white-space:pre-line;word-break:break-word;position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;padding:8px;font-size:12px;color:#ddd;background:rgba(0,0,0,.5);pointer-events:none} .mv-msg.err{pointer-events:auto;cursor:pointer} .mv-msg.park{pointer-events:auto;cursor:pointer;font-size:clamp(48px,20vw,92px);background:rgba(0,0,0,.18);text-shadow:0 2px 8px rgba(0,0,0,.7)} .mv-grid.multi .mv-msg.park{font-size:clamp(36px,12vw,56px)} .mv-grid.multi .mv-ctl{display:none!important} .mv-tile.bad{min-height:120px} .mv-tile.bad .mv-stg{aspect-ratio:auto;min-height:120px} .mv-tile.bad video{display:none} .mv-tile.bad .mv-msg{position:relative;inset:auto;padding:34px 12px 14px;font-size:11px;line-height:1.35} .mv-msg.hide{display:none} .mv-tile:fullscreen{aspect-ratio:auto;width:100%;height:100%;border-radius:0;outline:0} .mv-tile:-webkit-full-screen{aspect-ratio:auto;width:100%;height:100%;border-radius:0;outline:0} .mv-tile:fullscreen .mv-stg,.mv-tile:-webkit-full-screen .mv-stg{aspect-ratio:auto;flex:1;min-height:0} .mv-ctl{position:absolute;left:0;right:0;bottom:0;padding:22px 8px 6px;background:linear-gradient(transparent,rgba(0,0,0,.82));display:none;flex-direction:column;gap:2px;cursor:default;user-select:none;-webkit-user-select:none} .mv-tile.act.ui .mv-ctl{display:flex} .mv-tile.bad .mv-ctl{display:none!important} .mv-tile.nodvr .mv-sk,.mv-tile.nodvr .mv-tm{display:none} .mv-sk{position:relative;height:24px;touch-action:none;cursor:pointer;display:flex;align-items:center} .mv-sk-t{position:absolute;left:0;right:0;height:4px;border-radius:2px;background:rgba(255,255,255,.28)} .mv-sk-f{position:absolute;left:0;height:4px;border-radius:2px;background:var(--th,#3b82f6);width:0} .mv-sk-k{position:absolute;top:6px;width:12px;height:12px;margin-left:-6px;border-radius:50%;background:#fff;left:0;box-shadow:0 0 0 2px rgba(0,0,0,.35)} .mv-cr{display:flex;align-items:center;gap:2px} .mv-cb{flex:none;min-width:34px;height:32px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:0 6px;border:0;border-radius:8px;background:transparent;color:#fff;font-size:11px;font-weight:600;line-height:1;cursor:pointer} .mv-cb:hover{background:rgba(255,255,255,.16)} .mv-vol{--p:100%;flex:0 1 110px;min-width:64px;width:auto;height:36px;margin:0 6px;padding:0;background:transparent;touch-action:none;cursor:pointer;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent} .mv-vol::-webkit-slider-runnable-track{height:4px;border-radius:2px;background:linear-gradient(90deg,var(--th,#3b82f6) var(--p),rgba(255,255,255,.3) var(--p))} .mv-vol::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:22px;height:22px;margin-top:-9px;border-radius:50%;background:#fff;border:0;box-shadow:0 0 0 2px rgba(0,0,0,.35)} .mv-vol::-moz-range-track{height:4px;border-radius:2px;background:rgba(255,255,255,.3)} .mv-vol::-moz-range-progress{height:4px;border-radius:2px;background:var(--th,#3b82f6)} .mv-vol::-moz-range-thumb{width:22px;height:22px;border:0;border-radius:50%;background:#fff;box-shadow:0 0 0 2px rgba(0,0,0,.35)} .mv-lv{flex:none;height:22px;padding:0 9px;margin-left:4px;border:0;border-radius:6px;background:rgba(255,255,255,.2);color:#fff;font-size:10px;font-weight:700;line-height:22px;letter-spacing:.4px;cursor:pointer} .mv-lv.on{background:#e53935} .mv-qb{min-width:58px} .mv-tm{flex:none;margin-left:6px;color:#ddd;font-size:11px;font-variant-numeric:tabular-nums} .mv-sp{flex:1} .mv-qm{position:absolute;right:8px;bottom:50px;min-width:150px;max-height:70%;overflow-y:auto;padding:4px;border-radius:10px;background:rgba(20,20,24,.97);box-shadow:0 6px 20px rgba(0,0,0,.5);display:none;flex-direction:column;z-index:3} .mv-qm.open{display:flex} .mv-qi{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px;border-radius:7px;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap} .mv-qi:hover{background:rgba(255,255,255,.1)} .mv-qi.sel{color:var(--th,#3b82f6);font-weight:700} .mv-qs{height:1px;margin:4px 6px;background:rgba(255,255,255,.14)} .mv-empty{position:absolute;inset:0;display:flex;flex-direction:column;gap:12px;align-items:center;justify-content:center;text-align:center;padding:16px;color:var(--sk-mfg,#aaa);font-size:13px} .mv-empty .bm-tbtn{display:inline-flex;align-items:center;gap:6px} .mv-add{position:absolute;inset:0;display:none;flex-direction:column;gap:8px;background:transparent;z-index:2;overflow:hidden} .mv-add.open{display:flex} .mv-setp{display:none;flex:none;flex-direction:column;gap:2px;padding:4px 10px;border-radius:10px;background:rgba(0,0,0,.28)} .mv-setp.open{display:flex} .mv-stage.adding>.mv-grid,.mv-stage.adding>.mv-empty{visibility:hidden} .mv-r2{display:flex;flex:none;gap:8px;align-items:center} .mv-inp{flex:1;min-width:0;box-sizing:border-box;height:36px;padding:0 12px;border-radius:8px;border:1px solid var(--sk-in,rgba(255,255,255,.18));background:var(--sk-fill,rgba(255,255,255,.06));color:var(--sk-fg,#fff);font:inherit;font-size:14px;outline:0;user-select:text;-webkit-user-select:text} .mv-add .bm-tbtn{flex:none;white-space:nowrap} .mv-hd{display:flex;flex:none;align-items:center;gap:10px;font-weight:600;font-size:14px;color:var(--sk-fg,#fff)} .mv-sec{flex:none;font-size:12px;color:var(--sk-mfg,#aaa)} .mv-it.cur{background:rgba(255,255,255,.1);font-weight:600} .mv-add>.mv-inp{flex:none} .mv-list{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;color:var(--sk-mfg,#aaa);font-size:13px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent} .mv-it{display:flex;flex:none;align-items:center;gap:10px;padding:6px 10px;border-radius:10px;background:rgba(255,255,255,.05);cursor:pointer} .mv-it:hover{background:rgba(255,255,255,.11)} .mv-it.dis{opacity:.4;pointer-events:none} .mv-it img{width:52px;height:34px;object-fit:cover;border-radius:6px;background:#000;flex:none} .mv-it b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:13px;color:var(--sk-fg,#fff)} .mv-it i{font-style:normal;color:var(--sk-mfg,#9aa);font-size:11px;flex:none} .mv-it{user-select:none;-webkit-user-select:none;-webkit-touch-callout:none} .mv-it img{pointer-events:none} .mv-ck{display:none;flex:none;width:20px;height:20px;box-sizing:border-box;border-radius:50%;border:2px solid rgba(255,255,255,.4);align-items:center;justify-content:center;font-size:12px;line-height:1;color:#fff} .mv-list.selmode .mv-ck{display:flex} .mv-it.sel{background:rgba(255,255,255,.16);box-shadow:inset 0 0 0 2px var(--th,#3b82f6)} .mv-it.sel .mv-ck{background:var(--th,#3b82f6);border-color:var(--th,#3b82f6)} .mv-it.sel .mv-ck::after{content:"\\2713"} .mv-hd .mv-gob{margin-left:auto} .mv-tb button{font-size:10px;font-weight:700} .mv-tb button.rec{width:100%;padding:0 4px;background:#e53935;font-size:11px;font-variant-numeric:tabular-nums} .fav-modal.mode-mv .mv-pane{gap:12px;padding:12px} .mv-bar{gap:8px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,.07)} .mv-bar .bm-tbtn,.mv-bar .bm-hbtn{height:40px;margin:0;padding:0 14px;border-radius:12px;font-size:13px;font-weight:600;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);transition:background .15s,transform .1s,border-color .15s} .mv-bar .bm-tbtn.on{background:var(--th,#3b82f6);border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)} .mv-bar .bm-tbtn:not(.on):hover,.mv-bar .bm-hbtn:hover{background:rgba(255,255,255,.14);border-color:rgba(255,255,255,.22)} .mv-bar .bm-tbtn:active,.mv-bar .bm-hbtn:active{transform:scale(.96)} .mv-grid{gap:12px;padding:2px 2px 16px} .mv-tile{background:var(--sk-card,#16161a);border:1px solid rgba(255,255,255,.1);border-radius:16px;outline:0;box-shadow:0 4px 16px rgba(0,0,0,.45);transition:border-color .2s,box-shadow .2s} .mv-tile.act{border-color:var(--th,#3b82f6);box-shadow:0 0 0 1px var(--th,#3b82f6),0 6px 22px rgba(0,0,0,.55)} .mv-lbl{padding:9px 12px;font-size:13px;font-weight:700;background:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,.04));border-bottom:1px solid rgba(255,255,255,.06)} .mv-nm{letter-spacing:.2px} .mv-vw{padding:2px 8px;border-radius:10px;background:rgba(0,0,0,.35);font-size:11px;font-weight:600} .mv-stg{border-radius:0;overflow:hidden} .mv-tb{gap:8px;padding:8px 10px 10px;background:transparent;border-top:1px solid rgba(255,255,255,.06)} .mv-tb button{height:38px;border-radius:12px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);transition:background .15s,transform .1s,border-color .15s} .mv-tb button:hover{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.22)} .mv-tb button:active{transform:scale(.93)} .mv-tb button svg{display:block;flex:none} .mv-tb button.rec{background:#e53935;border-color:transparent;box-shadow:0 2px 10px rgba(229,57,53,.45)} .mv-tile.act .mv-tb button:nth-child(3){background:var(--th,#3b82f6);border-color:transparent} .mv-tile:fullscreen .mv-tb,.mv-tile:-webkit-full-screen .mv-tb{display:none} .mv-tile.bad .mv-tb{opacity:.85} .mv-ctl{padding:26px 10px 8px} .mv-qm{border-radius:12px;border:1px solid rgba(255,255,255,.1)} .mv-empty{gap:14px;font-size:14px} .mv-empty .bm-tbtn{height:42px;padding:0 20px;border-radius:14px;font-size:14px} .mv-add{padding:2px;gap:10px} .mv-hd{font-size:16px;font-weight:700;gap:12px} .mv-hd .bm-hbtn,.mv-hd .bm-tbtn{height:36px;padding:0 14px;border-radius:12px} .mv-sec{font-size:12px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;padding:2px 4px 0} .mv-inp{height:42px;border-radius:12px;padding:0 14px;transition:border-color .15s,box-shadow .15s} .mv-inp:focus{border-color:var(--th,#3b82f6);box-shadow:0 0 0 3px rgba(255,255,255,.08)} .mv-r2 .bm-tbtn{height:42px;padding:0 18px;border-radius:12px} .mv-list{gap:8px} .mv-it{gap:12px;padding:8px 12px;border-radius:14px;border:1px solid rgba(255,255,255,.07);transition:background .15s,border-color .15s,transform .1s} .mv-it:hover{border-color:rgba(255,255,255,.18)} .mv-it:active{transform:scale(.985)} .mv-it img{width:68px;height:42px;border-radius:8px} .mv-it b{font-size:14px} .mv-it i{padding:2px 8px;border-radius:10px;background:rgba(0,0,0,.3);font-weight:600} .mv-bar{flex-wrap:nowrap;gap:6px;padding-bottom:8px} .mv-bar .bm-tbtn,.mv-bar .bm-hbtn{height:34px;padding:0 10px;border-radius:10px;font-size:12px;gap:5px;white-space:nowrap;flex:0 1 auto;min-width:0} .mv-bar .bm-tbtn svg,.mv-bar .bm-hbtn svg{width:14px;height:14px} .mv-bar .bm-hbtn{min-width:34px;justify-content:center} .mv-grid{gap:10px} .mv-tile{border-radius:14px} .mv-lbl{padding:6px 10px;font-size:12px} .mv-vw{padding:1px 7px;font-size:10px} .mv-tb{gap:6px;padding:6px 8px 8px} .mv-tb button{height:30px;border-radius:9px;background:rgba(0,0,0,.45)} .mv-tb button svg{width:14px;height:14px} .mv-tb button.rec{font-size:10px} .mv-grid{grid-auto-rows:max-content;align-content:start} .mv-tile{flex:none;min-height:min-content} .mv-tile.bad{min-height:120px} .mv-tile:fullscreen,.mv-tile:-webkit-full-screen{min-height:0} .mv-grid::after{content:'';display:block;height:1px} .mv-tb{padding-bottom:10px} .mv-tb button svg,.mv-cb svg{display:block;flex:none;margin:0;overflow:visible} .mv-tile,.mv-tile *,.mv-grid,.mv-bar,.mv-bar *{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none} .mv-tile *::selection,.mv-tile::selection,.mv-bar *::selection{background:transparent;color:inherit} .mv-tile video::selection{background:transparent} .mv-tile button:focus,.mv-tile button:focus-visible,.mv-bar button:focus{outline:0} .mv-inp,.mv-inp *{-webkit-user-select:text;user-select:text} .mv-stage{flex:1 1 0;overflow:hidden} .mv-grid{position:absolute;inset:0;overflow-y:scroll;overflow-x:hidden;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior:contain;scrollbar-gutter:stable} .mv-list{overflow-y:scroll;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior:contain} .fav-modal.mode-mv .mv-pane{padding:10px 0 0;gap:10px} .fav-modal.mode-mv .mv-bar{margin-inline:0;padding-inline:14px;padding-bottom:10px;margin-bottom:0} .mv-grid{scrollbar-gutter:auto;padding:0 14px 16px} .mv-list{padding-inline:14px} .mv-add{padding:2px 14px} .mv-tb .mv-tbfs{display:none} .mv-grid.multi .mv-tb{grid-template-columns:repeat(4,minmax(0,1fr))} .mv-grid.multi .mv-tb .mv-tbfs{display:inline-flex} .mv-grid.multi .mv-tile.act.ui:fullscreen .mv-ctl,.mv-grid.multi .mv-tile.act.ui:-webkit-full-screen .mv-ctl{display:flex!important} .mv-tile:fullscreen,.mv-tile:-webkit-full-screen{box-sizing:border-box;border:0!important;border-radius:0!important;box-shadow:none!important;outline:0!important;background:#000!important;margin:0!important;padding:0!important;display:block!important;overflow:hidden!important;cursor:default} .mv-tile:fullscreen .mv-stg,.mv-tile:-webkit-full-screen .mv-stg{position:absolute!important;inset:0!important;width:100%!important;height:100%!important;aspect-ratio:auto!important;border-radius:0!important;flex:none!important} .mv-tile:fullscreen video,.mv-tile:-webkit-full-screen video{position:absolute;inset:0;width:100%!important;height:100%!important;object-fit:contain!important;background:#000} .mv-tile:fullscreen .mv-lbl,.mv-tile:-webkit-full-screen .mv-lbl{position:absolute;top:0;left:0;right:0;z-index:3;border:0;border-radius:0;background:linear-gradient(rgba(0,0,0,.78),transparent);padding:calc(10px + env(safe-area-inset-top,0px)) calc(14px + env(safe-area-inset-right,0px)) 22px calc(14px + env(safe-area-inset-left,0px));opacity:0;transition:opacity .2s;pointer-events:none} .mv-tile.ui:fullscreen .mv-lbl,.mv-tile.ui:-webkit-full-screen .mv-lbl{opacity:1} .mv-tile:fullscreen .mv-ctl,.mv-tile:-webkit-full-screen .mv-ctl{z-index:3;padding:26px calc(14px + env(safe-area-inset-right,0px)) calc(8px + env(safe-area-inset-bottom,0px)) calc(14px + env(safe-area-inset-left,0px))} .mv-tile:fullscreen .mv-qm,.mv-tile:-webkit-full-screen .mv-qm{z-index:4} `; const MV_IC = { add: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>', close: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>' }; MV_IC.play = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>'; MV_IC.pause = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M7 5h4v14H7zM13 5h4v14h-4z"/></svg>'; MV_IC.rew = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M11 6v12l-8.5-6zM21 6v12l-8.5-6z"/></svg>'; MV_IC.pip = icons.pip.replace('width="20" height="20"', 'width="18" height="18"'); MV_IC.fs = icons.fs.replace('width="20" height="20"', 'width="18" height="18"'); MV_IC.on = icons.vol.replace('width="20" height="20"', 'width="16" height="16"'); MV_IC.off = icons.mute.replace('width="20" height="20"', 'width="16" height="16"'); MV_IC.snap = icons.snap.replace('width="20" height="20"', 'width="16" height="16"'); MV_IC.rec = icons.rec.replace('width="20" height="20"', 'width="16" height="16"'); const mvT = k => L()[k] || i18n.EN[k] || ''; const saveRecording = async (blob, ext, durMs, name, disk) => { const fix = async (fn, fb) => { try { return await fn(); } catch { return fb; } }; if (ext === 'webm') { blob = await fix(() => MediaFix.stripSeekHead(blob), blob); const d = await fix(() => MediaFix.webmDuration(blob), 0); blob = await fix(() => fixWebmDuration(blob, d > 0 ? d : durMs), blob); blob = await fix(() => MediaFix.webm(blob), blob); } else if (ext === 'mp4') blob = await fix(() => MediaFix.mp4(blob), blob); Helpers.download(blob, name); if (disk) disk.markDone().finally(() => disk.dispose()); }; const newMediaRecorder = (stream, mimeType, q) => { const [vBps, aBps] = REC_Q[q] || REC_Q.hi; for (const opts of [{ mimeType, videoBitsPerSecond: vBps, audioBitsPerSecond: aBps }, { mimeType }, undefined]) { try { return new MediaRecorder(stream, opts); } catch (e) { dbg(e, 'newMediaRecorder'); } } return null; }; const newHlsCapture = ({ url, fetchBin, cfg, base, save, onProgress }) => new HlsKit.HlsCapture({ url, fetchBin, onProgress, maxHeight: REC_H[cfg.q] ?? Infinity, partBytes: cfg.part * 1048576, totalBytes: cfg.total * 1048576, makeSink: no => new HlsKit.DiskBuffer({ meta: { name: base, ext: 'mp4', mime: 'video/mp4', part: no, hls: 1 } }), onPart: (blob, no, info) => save(blob, `${base}${no > 1 || !info.final ? `_PT${no}` : ''}.mp4`, info.sink) }); const MV_DVR_OPTS = [0, 60, 120, 300]; const mvKeep = () => Math.max(2, mvDvr()); const mvDvr = () => { const v = Store.get('bc_mv_dvr', 120); return MV_DVR_OPTS.includes(v) ? v : 120; }; const mvAbr = () => Store.get('bc_mv_abr', true) !== false; const mvBg = () => Store.get('bc_mv_bg', true) !== false; const mvAbrLast = () => { const o = Store.get('bc_mv_abr_h', null); return isObj(o) && o.h > 0 && Date.now() - o.t < 1800000 ? o.h : 0; }; const ABR_UP_MS = 90000; const ABR_BACK0 = 120000; const ABR_LOW = 0.3; const MvNet = { c: navigator.connection || navigator.mozConnection || navigator.webkitConnection || null, cap() { const t = this.c && this.c.effectiveType; if (t === 'slow-2g' || t === '2g') return 1; if (t === '3g') return 480; return 0; }, watch(fn) { if (this.c && this.c.addEventListener) this.c.addEventListener('change', fn); } }; const mvGet = (url, referer, range) => gmBytes(url, referer, true, range); const mvUrl = u => (isStr(u) ? u.replace(/\\\//g, '/').replace(/^\/\//, 'https://') : ''); const mvFindM3u8 = (x, d = 0) => { if (d > 6 || x == null) return ''; if (isStr(x)) { const u = mvUrl(x); return /^https?:\/\/[^\s"']+\.m3u8(\?[^\s"']*)?$/i.test(u) ? u : ''; } if (typeof x !== 'object') return ''; for (const k of Object.keys(x)) { const r = mvFindM3u8(x[k], d + 1); if (r) return r; } return ''; }; const mvFindKey = (x, key, d = 0) => { if (d > 6 || !x || typeof x !== 'object') return ''; if (isStr(x[key]) && x[key]) return x[key]; for (const k of Object.keys(x)) { const r = mvFindKey(x[k], key, d + 1); if (r) return r; } return ''; }; const mvJson = async (url, ref) => { try { return JSON.parse((await requestOnce(url, { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Referer': ref })) || ''); } catch { return null; } }; const mvShort = v => String(v).replace(/[A-Za-z0-9_\-+/=]{20,}/g, '<t>').slice(0, 56); const mvScan = (x, path = '', out = [], d = 0) => { if (d > 6 || x == null || out.length >= 8) return out; if (typeof x === 'string') { if (x && /video|stream|hls|server|host|edge/i.test(path)) out.push(`${path}=${mvShort(x)}`); return out; } if (typeof x !== 'object') return out; for (const k of Object.keys(x)) mvScan(x[k], path ? `${path}.${k}` : k, out, d + 1); return out; }; const BC_TOKEN_RES = [/data-csrf_value=["']([^"']+)["']/i, /name=["']_csrf_token["'][^>]*?value=["']([^"']+)["']/i, /value=["']([^"']+)["'][^>]*?name=["']_csrf_token["']/i, /csrf[\w-]*["'\s:=]+["']?([a-f0-9]{24,128})(?![\w-])/i]; const bcCsrfField = html => (String(html).match(/data-csrf_field=["']([^"']+)["']/i) || [])[1] || '_csrf_token'; const bcTokenBag = () => { const list = []; return { list, add: (t, from, field) => { if (t && !list.some(x => x.t === t)) list.push({ t, from, field: field || '_csrf_token' }); } }; }; const bcHtmlTokens = (html, bag, own) => { for (const re of BC_TOKEN_RES) { const m = html.match(re); if (m) { bag.add(m[1], 'html', bcCsrfField(html)); break; } } if (own) bag.add((document.cookie.match(/csrf[\w-]*=([^;]+)/i) || [])[1], 'cookie'); }; const bcHtmlDebug = (html, tokens) => { const cs = html.search(/csrf/i); return `html:${html.length}b tokens:${tokens.map(x => x.from).join('+') || 'no'} csrf@${cs}${cs >= 0 ? `[${mvShort(html.slice(Math.max(0, cs - 12), cs + 48).replace(/\s+/g, ' '))}]` : ''}`; }; const BC_VS_KEYS = ['videoServerUrl', 'videoServer', 'hlsServerUrl', 'streamServerUrl']; const bcFindVs = x => { for (const k of BC_VS_KEYS) { const v = mvFindKey(x?.localData, k) || mvFindKey(x, k); if (isStr(v) && /^(https?:)?\/\//.test(mvUrl(v))) return mvUrl(v); } return ''; }; const bcPick = (st, x) => { if (!x) return; st.vs = st.vs || bcFindVs(x); st.direct = st.direct || mvFindM3u8(x); st.guess = st.guess || bcEdgeGuess(mvFindKey(x, 'frame')); }; const bcFound = st => !!(st.vs || st.direct); const bcScanHtml = (html, st) => { const hv = html.match(new RegExp(`(?:${BC_VS_KEYS.join('|')})["']?\\s*[:=]\\s*["']([^"']+)["']`, 'i')); if (hv) st.vs = mvUrl(hv[1]); st.guess = st.guess || bcEdgeGuess(html); const hm = html.match(/https?:\\?\/\\?\/[^\s"'<>\\]+?\.m3u8[^\s"'<>\\]*/i); if (hm) st.direct = mvUrl(hm[0]); }; const bcAmfTry = async (o, slug, k, st) => { const body = `method=getRoomData&args%5B%5D=${encodeURIComponent(slug)}&args%5B%5D=&args%5B%5D=${k.t ? `&${encodeURIComponent(k.field || '_csrf_token')}=${encodeURIComponent(k.t)}` : ''}`; const raw = await requestPostOnce(`${o}/tools/amf.php?m=1&t=${Date.now()}`, body, { Referer: `${o}/${encodeURIComponent(slug)}`, Origin: o }); const j = JSON.parse(raw || ''); st.dbg.push(`amf(${k.from}):${String(raw).length}b ${j?.status || ''}`); bcPick(st, j); const un = j?.performerData?.username; if (isStr(un) && un) st.name = un; if (bcFound(st)) return true; const sc = mvScan(j); if (sc.length) st.dbg.push(`amf-scan:${sc.join(' ')}`); return false; }; const bcAmf = async (o, slug, tokens, st) => { const tries = tokens.length ? tokens.slice(0, 2) : [{ t: '', from: 'none' }]; for (const k of tries) { let found = false; try { found = await bcAmfTry(o, slug, k, st); } catch { st.dbg.push(`amf(${k.from}):fail`); } if (found) break; } }; const bcChat = async (o, slug, st) => { const j = await mvJson(`${o}/get-member-chat-data?username=${encodeURIComponent(slug)}&withMiniProfile=1&liveTab=female`, `${o}/${encodeURIComponent(slug)}`); st.dbg.push(j ? 'chat:ok' : 'chat:fail'); bcPick(st, j); if (!j || bcFound(st)) return; const sc = mvScan(j); st.dbg.push(`chat-scan:${sc.join(' ') || 'nothing'}`); st.dbg.push(`chat-keys:${Object.keys(j.result || {}).join(',')}`.slice(0, 260)); }; const bcServerTag = st => st.vs ? originOf(st.vs) || 'bad' : (st.direct ? 'direct' : 'none'); const BC_PLAYLIST_PATHS = ['/hls/stream_{n}/playlist.m3u8']; const bcEdgeGuess = txt => { const m = String(txt || '').replace(/\\\//g, '/').match(/\/\/mobile-edge(\d+)\.(bcvcdn\.com)/i); return m ? `https://live-edge${m[1]}.${m[2]}` : ''; }; const bcPlaylistCandidates = st => { if (st.direct) return [st.direct]; const bases = [...new Set([st.vs, st.guess].filter(Boolean).map(b => b.replace(/\/$/, '')))]; const names = [...new Set([st.name, String(st.name).toLowerCase()])]; return bases.flatMap(b => BC_PLAYLIST_PATHS.flatMap(t => names.map(n => b + t.replace('{n}', n)))); }; const bcProbePlaylist = async (st, ref) => { const list = bcPlaylistCandidates(st); if (st.direct || !list.length) return list[0] || ''; for (const u of list) { let txt = ''; try { txt = await requestOnce(u, { Referer: ref }); } catch (e) { dbg(e, 'bcProbePlaylist'); } if (isStr(txt) && txt.startsWith('#EXTM3U')) { st.dbg.push(`playlist:ok ${u === list[0] ? 'primary' : 'alt'}`); return u; } } st.dbg.push('playlist:unverified'); return st.vs ? list[0] : ''; }; const cbDossier = html => { try { const m = String(html).match(/initialRoomDossier\s*=\s*("(?:[^"\\]|\\.)*")/); if (!m) return null; let o = JSON.parse(m[1]); if (isStr(o)) o = JSON.parse(o); return o && typeof o === 'object' ? o : null; } catch { return null; } }; const cbPageHls = html => { const o = cbDossier(html); const u = o ? (mvUrl(o.hls_source) || mvFindM3u8(o)) : ''; if (u) return u; const r = String(html).match(/(?:"|\\u0022)hls_source(?:"|\\u0022)\s*:\s*(?:"|\\u0022)(https?:(?:(?!\\u0022|")(?:[^\\]|\\u[0-9a-f]{4}))+)/i); if (!r) return ''; try { return mvUrl(JSON.parse(`"${r[1]}"`)); } catch { return ''; } }; const mvVault = (() => { const DB = 'bcu_warm', ST = 'hls', TTL = 2 * 3600000, MAX = 80; const mem = new Map(); let dbp = null, loaded = null; const open = () => dbp || (dbp = new Promise(res => { try { const r = indexedDB.open(DB, 2); r.onupgradeneeded = () => { if (!r.result.objectStoreNames.contains(ST)) r.result.createObjectStore(ST); }; r.onsuccess = () => res(r.result); r.onerror = r.onblocked = () => res(null); } catch { res(null); } })); const store = async mode => { const db = await open(); try { return db ? db.transaction(ST, mode).objectStore(ST) : null; } catch { return null; } }; const preload = () => loaded || (loaded = (async () => { const s = await store('readonly'); if (!s) return; await new Promise(res => { const c = s.openCursor(); c.onsuccess = () => { const cur = c.result; if (!cur) return res(); mem.set(cur.key, cur.value); cur.continue(); }; c.onerror = () => res(); }); })()); const fresh = r => !!r && Date.now() - r.at < TTL; const trim = () => { if (mem.size <= MAX) return; const old = [...mem.entries()].sort((a, b) => a[1].at - b[1].at).slice(0, mem.size - MAX); for (const [k] of old) { mem.delete(k); store('readwrite').then(s => s && s.delete(k)).catch(() => {}); } }; return { ready: () => Promise.race([preload(), new Promise(r => setTimeout(r, 300))]), peek: k => { const r = mem.get(k); return fresh(r) ? r : null; }, keep: (k, patch) => { const r = { ...(mem.get(k) || {}), ...patch, at: Date.now() }; mem.set(k, r); trim(); store('readwrite').then(s => s && s.put(r, k)).catch(() => {}); }, forget: k => { mem.delete(k); store('readwrite').then(s => s && s.delete(k)).catch(() => {}); } }; })(); const mvVaultKey = e => { try { if (e.t === 'url') { const u = new URL(e.url); return `u:${u.origin}${u.pathname}`; } const site = urlSite(e.url), slug = modelSlug(e.url); return site && slug ? `${site}/${String(slug).toLowerCase()}` : ''; } catch { return ''; } }; const mvIsSelf = e => { try { return e.t !== 'url' && urlSite(e.url) === siteKey(location.hostname) && String(e.name).toLowerCase() === String(currentSite.getName()).toLowerCase() && !!getStreamUrl(); } catch { return false; } }; const MV_RES = { cb: async (e, slug) => { const j = await mvJson(`https://chaturbate.com/api/chatvideocontext/${encodeURIComponent(slug)}/`, `https://chaturbate.com/${encodeURIComponent(slug)}/`); let url = mvUrl(j?.hls_source) || mvFindM3u8(j); if (!url) url = cbPageHls((await requestHtml(e.url)) || ''); return { url, ref: 'https://chaturbate.com/' }; }, sc: async (e, slug) => { const o = originOf(e.url) || 'https://stripchat.com', ref = `${o}/`; const j = await mvJson(`${o}/api/front/v2/models/username/${encodeURIComponent(slug)}/cam`, `${o}/${encodeURIComponent(slug)}`); const direct = mvFindM3u8(j); if (direct) return { url: direct, ref }; const ids = [j?.cam?.streamName, j?.user?.user?.id, j?.user?.id].filter(v => v != null && v !== '').map(String); for (const id of [...new Set(ids)]) for (const host of ['doppiocdn.com', 'doppiocdn.net', 'doppiocdn.org', 'doppiocdn.media']) { const cand = `https://edge-hls.${host}/hls/${id}/master/${id}_auto.m3u8`; const txt = await requestOnce(cand, { Referer: ref }); if (txt && txt.startsWith('#EXTM3U')) return { url: cand, ref }; } return { url: '', ref }; }, bc: async (e, slug) => { const o = originOf(e.url), own = o === location.origin, bag = bcTokenBag(); const st = { name: slug, vs: '', direct: '', dbg: [] }; if (own) bag.add(bcCsrfToken(), 'page'); const html = (await requestHtml(e.url)) || ''; bcHtmlTokens(html, bag, own); st.dbg.push(bcHtmlDebug(html, bag.list)); bcScanHtml(html, st); if (!bcFound(st)) await bcAmf(o, slug, bag.list, st); if (!bcFound(st)) await bcChat(o, slug, st); st.dbg.push(`server:${bcServerTag(st)}`); const url = await bcProbePlaylist(st, `${o}/`); return { url, ref: `${o}/`, dbg: st.dbg.join('\n') }; } }; const mvResolve = async e => { if (e.t === 'url') return { url: e.url, ref: `${originOf(e.url)}/` }; const site = urlSite(e.url), slug = modelSlug(e.url); if (site === siteKey(location.hostname) && String(e.name).toLowerCase() === String(currentSite.getName()).toLowerCase()) { const cur = getStreamUrl(); if (cur) return { url: cur, ref: `${location.origin}/` }; } const fn = MV_RES[site]; return fn && slug ? fn(e, slug) : { url: '' }; }; const MV_FLT = [ ['bc_mv_hide_off', true, 'bm_online', 'bm_online_t', ''], ['bc_mv_hide_priv', true, 'bm_priv', 'st_private', 'st_private'], ['bc_mv_hide_group', false, 'st_group', 'st_group', 'st_group'], ['bc_mv_hide_vip', false, 'st_vip', 'st_vip', 'st_vip'], ['bc_mv_hide_away', false, 'st_away', 'st_away', 'st_away'] ]; const mvFltOn = (key, def) => Store.get(key, def) === true; const mvErr = x => (x ? String(x.message || x).slice(0, 80) : 'ended'); class MvTile { constructor(mv, e) { this.mv = mv; this.e = e; this.gen = 0; this.tries = 0; this.act = false; this.player = null; this.rt = 0; this.q = 0; this.vh = []; this.um = false; this.drag = false; this.uat = 0; this.ti = 0; this.abrH = 0; this.abrAt = 0; this.abrTimer = 0; this.abrBack = ABR_BACK0; this.abrBlock = {}; this.stalls = []; this.lastStall = 0; this.waitAt = 0; this.okSince = 0; this.lowN = 0; this.savedAt = 0; const el = this.el = mk('div', 'mv-tile'); const v = this.video = document.createElement('video'); v.muted = true; v.volume = Math.min(1, Math.max(0, Number(Store.get('bc_mv_vol', 1)) || 0)); v.playsInline = true; v.setAttribute('playsinline', ''); this.lbl = textEl('mv-lbl', e.name || '', 'span'); this.msg = mk('div', 'mv-msg'); const tb = mk('div', 'mv-tb'); this.sndBtn = mk('button'); this.sndBtn.innerHTML = MV_IC.off; this.sndBtn.onclick = ev => { ev.stopPropagation(); this.mv.select(this); }; const rm = mk('button'); rm.innerHTML = MV_IC.close; Helpers.bindLongPress(rm, () => this.idle(), () => this.mv.remove(this), 700); rm.onclick = ev => ev.stopPropagation(); this.shotBtn = mk('button'); this.shotBtn.innerHTML = MV_IC.snap; this.shotBtn.title = L().snap; this.shotBtn.onclick = ev => { ev.stopPropagation(); this.shot(); }; this.recBtn = mk('button'); this.recBtn.innerHTML = MV_IC.rec; this.recBtn.title = L().rec_tip || L().rec; this.recBtn.style.touchAction = 'none'; this.recBtn.onclick = ev => ev.stopPropagation(); Helpers.bindLongPress(this.recBtn, () => this.toggleRec(), () => this.mv.ui.recorder?.openSettings()); this.recS = null; this.tbFs = mk('button', 'mv-tbfs'); this.tbFs.innerHTML = MV_IC.fs; this.tbFs.title = mvT('mv_fs'); this.tbFs.onclick = ev => { ev.stopPropagation(); if (!this.act) this.mv.select(this); this.showUi(); this.toggleFs(); }; tb.append(this.shotBtn, this.recBtn, rm, this.tbFs); const stg = this.stg = mk('div', 'mv-stg'); stg.append(v, this.msg); el.append(this.lbl, stg, tb); this.buildCtl(); this.setPoster(); el.onclick = () => { if (this.mv.grid.classList.contains('multi') && !this.isFs()) { if (!this.act) { this.mv.select(this); return; } if (this.player) this.togglePlay(); return; } if (!this.act) { this.mv.select(this); this.showUi(); } else this.toggleUi(); }; this.msg.onclick = ev => { if (this.msg.classList.contains('err') || this.msg.classList.contains('park')) { ev.stopPropagation(); this.tries = 0; if (!this.act) this.mv.select(this); this.start(); } }; v.onplaying = () => { this.msg.classList.add('hide'); this.tries = 0; this.sw = 0; this.played = true; this.setWant(!this.userPause); this.syncPlay(); }; v.onpause = () => { this.syncPlay(); if (mvBg() && !this.userPause && !this.parked && this.player) setTimeout(() => this.bgResume(), 200); }; v.addEventListener('enterpictureinpicture', () => { this.pip = true; this.pipKeep(true); }); v.addEventListener('leavepictureinpicture', () => { this.pip = false; this.pipKeep(false); if (!this.mv.isOpen() && this.player && !mvBg()) this.idle(); }); v.onplay = () => { this.syncPlay(); if (mvDvr() === 0 && this.player && this.player.shift) this.goLive(); }; v.onresize = () => this.paintLbl(); v.addEventListener('waiting', () => { const now = Date.now(); if (!this.abrReady(now)) return; if (!this.waitAt) this.waitAt = now; this.abrStall(false); }); v.addEventListener('playing', () => { this.waitAt = 0; }); this.paintLbl(); this.lblTimer = setInterval(() => { if (this.el.getClientRects().length) this.paintLbl(); }, 4000); } viewersNow() { try { const k = bmKey(this.e.url || ''); const b = k && AppState.bookmarks.find(x => isObj(x) && isStr(x.url) && bmKey(x.url) === k); return b ? bmViews(b) : 0; } catch { return 0; } } paintLbl() { const h = this.video.videoHeight, vw = this.viewersNow(); const key = `${h || 0}|${vw}`; if (this._lk === key) return; this._lk = key; this.lbl.innerHTML = `<span class="mv-nm">${escHTML(this.e.name || '')}${h ? ` · ${h}p` : ''}</span>${vw ? `<span class="mv-vw">${icons.eye}${fmtCount(vw)}</span>` : ''}`; } flashBtn(btn, txt, back) { btn.classList.remove('rec'); btn.textContent = txt; setTimeout(back, 1200); } paintShot() { this.shotBtn.innerHTML = MV_IC.snap; } paintRec(sec) { const b = this.recBtn; if (!this.recS) { b.classList.remove('rec'); b.innerHTML = MV_IC.rec; return; } b.classList.add('rec'); b.textContent = Helpers.formatTime(sec || 0); } shot() { const v = this.video, back = () => this.paintShot(); if (!v.videoWidth) return this.flashBtn(this.shotBtn, 'ERR', back); Helpers.snap(v, this.e.name, ok => this.flashBtn(this.shotBtn, ok ? 'OK' : 'ERR', back)); } toggleRec() { if (this.recS) { try { this.recS.stop(); } catch (e) { dbg(e, 'MvTile.toggleRec'); } return; } const cfg = recCfg(), base = `${this.e.name || 'REC'}_${Helpers.getTimestamp()}`; const t0 = Date.now(); let timer = 0; const begin = ctl => { this.recS = ctl; this.paintRec(0); timer = setInterval(() => this.paintRec(Math.floor((Date.now() - t0) / 1000)), 1000); }; const end = () => { clearInterval(timer); this.recS = null; this.paintRec(0); }; const fail = t => this.flashBtn(this.recBtn, t || 'ERR', () => this.paintRec(0)); const capture = () => { const v = this.video; const st = v.captureStream ? v.captureStream(30) : (v.mozCaptureStream ? v.mozCaptureStream(30) : null); if (!st) return fail(); const mt = recMimeFor(cfg.fmt), ext = mt.includes('mp4') ? 'mp4' : 'webm'; const mr = newMediaRecorder(st, mt, cfg.q); if (!mr) return fail(); const disk = new HlsKit.DiskBuffer({ meta: { name: base, ext, mime: mt.split(';')[0] } }); const t1 = performance.now(); let bytes = 0; mr.ondataavailable = e => { if (!(e.data && e.data.size > 0)) return; disk.append(e.data); bytes += e.data.size; if (cfg.total && bytes >= cfg.total * 1048576) { try { mr.stop(); } catch (e) { dbg(e, 'toggleRec.capture'); } } }; mr.onerror = () => { try { mr.stop(); } catch (e) { dbg(e, 'toggleRec.capture'); } }; mr.onstop = () => { try { st.getTracks().forEach(t => t.stop()); } catch (e) { dbg(e, 'toggleRec.capture'); } if (disk.size > 0) disk.finish().then(b => saveRecording(b, ext, performance.now() - t1, `${base}.${ext}`, disk)).catch(() => {}); else disk.remove(); end(); }; try { mr.start(1000); } catch { end(); return fail(); } begin({ stop: () => mr.stop() }); }; const si = this.srcInfo; if (cfg.src === 'hls' && si && si.url) { let user = false; const cap = newHlsCapture({ url: si.url, cfg, base, fetchBin: (u, r) => mvGet(u, si.ref, r), save: (blob, name, sink) => saveRecording(blob, 'mp4', 0, name, sink) }); begin({ stop: () => { user = true; cap.stop(); } }); cap.start().then(res => { end(); if (res.error && !user && !res.stats.segments) return capture(); if (res.error && !user) fail(); }); return; } capture(); } say(text, err) { this.msg.textContent = text; this.msg.classList.toggle('err', !!err); this.el.classList.toggle('bad', !!err); this.msg.classList.remove('hide'); } setActive(on) { this.act = on; this.el.classList.toggle('act', on); this.applyAudio(); if (on) { this.showUi(); clearInterval(this.ti); this.ti = setInterval(() => this.tickUi(), 250); } else this.endUi(); } applyAudio() { const on = this.act && !this.um && this.video.volume > 0; this.video.muted = !this.act || this.um; this.sndBtn.innerHTML = this.act && !this.um ? MV_IC.on : MV_IC.off; if (this.muteBtn) this.muteBtn.innerHTML = on ? MV_IC.on : MV_IC.off; if (this.volEl) this.volEl.style.setProperty('--p', (this.video.volume * 100) + '%'); } endUi() { clearInterval(this.ti); this.ti = 0; this.el.classList.remove('ui'); this.menu?.classList.remove('open'); this.drag = false; } showUi() { this.el.classList.add('ui'); this.uat = Date.now(); } toggleUi() { if (this.el.classList.contains('ui')) { this.el.classList.remove('ui'); this.menu.classList.remove('open'); } else this.showUi(); } syncPlay() { if (this.playBtn) this.playBtn.innerHTML = this.video.paused ? MV_IC.play : MV_IC.pause; } range() { const b = this.video.buffered; return b.length ? [b.start(0), b.end(b.length - 1)] : null; } seekTo(t) { const v = this.video, r = this.range(), p = this.player; if (!r) return; const to = Math.min(Math.max(t, r[0] + 0.05), r[1] - 0.3); if (p) p.shift = r[1] - to > 3; try { v.currentTime = to; } catch (e) { dbg(e, 'MvTile.seekTo'); } } goLive() { const v = this.video, r = this.range(), p = this.player; if (p) { p.shift = false; p.o.hold = false; } if (r) { try { v.currentTime = r[1] - 1.5; } catch (e) { dbg(e, 'MvTile.goLive'); } } v.play().catch(() => {}); } togglePlay() { const v = this.video, p = this.player; if (v.paused) { this.userPause = false; this.setWant(true); if (p) p.o.hold = false; if (mvDvr() === 0) this.goLive(); else v.play().catch(() => {}); } else { this.userPause = true; this.setWant(false); if (p) { p.o.hold = true; p.shift = true; } v.pause(); } } bgResume() { const v = this.video, p = this.player; if (!mvBg() || this.userPause || this.parked || !p || p.dead || !v.isConnected || v.ended || !v.paused) return; if (Store.get('bc_sleep_lock', 0)) return; p.o.hold = false; if (mvDvr() === 0) this.goLive(); else v.play().catch(() => {}); } isFs() { const el = this.el; try { return el.matches(':fullscreen'); } catch { try { return el.matches(':-webkit-full-screen'); } catch { return false; } } } toggleFs() { if (this.isFs()) fsExit(); else fsRequest(this.el); } togglePip() { const v = this.video; if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); return; } if (document.pictureInPictureEnabled && v.requestPictureInPicture) v.requestPictureInPicture().catch(() => {}); } buildCtl() { const v = this.video; const ctl = this.ctl = mk('div', 'mv-ctl'); const stop = ev => { ev.stopPropagation(); this.uat = Date.now(); }; ctl.onclick = stop; ctl.onpointerdown = ev => { this.uat = Date.now(); ev.stopPropagation(); }; const sk = this.sk = mk('div', 'mv-sk'); this.skFill = mk('div', 'mv-sk-f'); this.skKnob = mk('div', 'mv-sk-k'); sk.append(mk('div', 'mv-sk-t'), this.skFill, this.skKnob); const frac = ev => { const r = sk.getBoundingClientRect(); return Math.min(1, Math.max(0, (ev.clientX - r.left) / (r.width || 1))); }; const paint = f => { this.skFill.style.width = `${f * 100}%`; this.skKnob.style.left = `${f * 100}%`; }; sk.onpointerdown = ev => { ev.stopPropagation(); this.drag = true; try { sk.setPointerCapture(ev.pointerId); } catch (e) { dbg(e, 'MvTile.buildCtl'); } paint(frac(ev)); }; sk.onpointermove = ev => { if (this.drag) paint(frac(ev)); }; const fin = ev => { if (!this.drag) return; this.drag = false; const r = this.range(); if (r) this.seekTo(r[0] + frac(ev) * (r[1] - r[0])); }; sk.onpointerup = fin; sk.onpointercancel = () => { this.drag = false; }; this.paintSk = paint; const row = mk('div', 'mv-cr'); const btn = (cls, title, fn) => { const b = mk('button', cls); b.title = title; b.onclick = ev => { ev.stopPropagation(); this.uat = Date.now(); fn(ev); }; return b; }; this.playBtn = btn('mv-cb', mvT('mv_play'), () => this.togglePlay()); this.muteBtn = btn('mv-cb', mvT('mv_mute'), () => { this.um = !this.um; if (!this.um && v.volume === 0) { v.volume = 0.5; this.volEl.value = '0.5'; } this.applyAudio(); }); const vol = this.volEl = mk('input', 'mv-vol'); vol.type = 'range'; vol.min = '0'; vol.max = '1'; vol.step = '0.05'; vol.value = String(v.volume); vol.oninput = () => { v.volume = +vol.value; if (v.volume > 0) this.um = false; Store.set('bc_mv_vol', v.volume); this.applyAudio(); this.uat = Date.now(); }; vol.onclick = ev => ev.stopPropagation(); this.lvBtn = btn('mv-lv', mvT('mv_live'), () => this.goLive()); this.lvBtn.textContent = mvT('mv_live'); this.tm = mk('span', 'mv-tm'); const sp = mk('div', 'mv-sp'); this.qBtn = btn('mv-cb mv-qb', mvT('mv_qual'), () => this.toggleMenu()); this.pipBtn = btn('mv-cb', mvT('mv_pip'), () => this.togglePip()); this.pipBtn.innerHTML = MV_IC.pip; this.fsBtn = btn('mv-cb', mvT('mv_fs'), () => this.toggleFs()); this.fsBtn.innerHTML = MV_IC.fs; if (!(document.pictureInPictureEnabled && v.requestPictureInPicture)) this.pipBtn.style.display = 'none'; row.append(this.playBtn, this.muteBtn, vol, this.lvBtn, this.tm, sp, this.qBtn, this.pipBtn, this.fsBtn); ctl.append(sk, row); const menu = this.menu = mk('div', 'mv-qm'); menu.onclick = stop; menu.onpointerdown = ev => ev.stopPropagation(); this.stg.append(ctl, menu); this.syncPlay(); this.applyAudio(); this.applyDvr(); } applyDvr() { this.el.classList.toggle('nodvr', mvDvr() === 0); } toggleMenu() { const m = this.menu; if (m.classList.contains('open')) { m.classList.remove('open'); return; } const st = this.player && this.player.stats; if (st && st.variants && st.variants.length) this.vh = st.variants; const hs = [...new Set(this.vh.map(x => x.h).filter(Boolean))].sort((a, b) => b - a); const rows = []; const item = (label, sel, fn) => { const r = mk('div', `mv-qi${sel ? ' sel' : ''}`); r.textContent = label; r.onclick = ev => { ev.stopPropagation(); fn(); }; rows.push(r); }; item(mvT('mv_auto'), !this.q, () => { this.q = 0; m.classList.remove('open'); this.start(); }); hs.forEach(h => item(`${h}p`, this.q === h, () => { this.q = h; m.classList.remove('open'); this.start(); })); m.replaceChildren(...rows); m.classList.add('open'); } paintProgress(r) { if (!r || this.drag) return; const span = Math.max(r[1] - r[0], 0.001); this.paintSk(Math.min(1, Math.max(0, (this.video.currentTime - r[0]) / span))); } qualityLabel() { if (this.q) return `${this.q}p`; const h = this.video.videoHeight; return h ? `${mvT('mv_auto')} ${h}p` : mvT('mv_auto'); } shouldHideUi() { return !this.video.paused && !this.drag && !this.menu.classList.contains('open') && Date.now() - this.uat > 3500; } tickUi() { const v = this.video, el = this.el; if (!el.classList.contains('ui')) return; const r = this.range(); this.paintProgress(r); this.qBtn.textContent = this.qualityLabel(); if (this.sw && Date.now() - this.sw < 15000) { if (this.shouldHideUi()) el.classList.remove('ui'); return; } const behind = r ? Math.max(0, r[1] - v.currentTime) : 0; const live = this.isLv = this.isLv ? behind < 9 : behind < 4; this.lvBtn.classList.toggle('on', live && !v.paused); this.tm.textContent = live ? '' : `\u2212${fmtDur(behind)}`; if (this.shouldHideUi()) el.classList.remove('ui'); } setPoster() { const u = this.e.url; const b = this.mv.ui.bookmarkManager?.prevEn === false ? null : (AppState.bookmarks || []).find(x => isObj(x) && x.frame && (x.url === u || (typeof bmKey === 'function' && bmKey(x.url) === bmKey(u)))); this.posterBase = b ? b.frame : ''; if (b) { this.video.poster = b.frame; this.video.setAttribute('referrerpolicy', 'no-referrer'); } else { this.video.removeAttribute('poster'); if (this.mv.ui.bookmarkManager?.prevEn !== false) this.avatarPoster(); } } async avatarPoster() { const u = this.e.url, k = (this.apTok = (this.apTok || 0) + 1); const bm = (AppState.bookmarks || []).find(x => isObj(x) && (x.url === u || bmKey(x.url) === bmKey(u))); let h = null; try { h = histLoad().m[bmKey(u)]; } catch (e) { dbg(e, 'MvTile.avatarPoster'); } const list = [bm && bm.avatar, h && h.a].map(toHttps).filter(Boolean); for (const a of list) { const d = await loadFrame(a); if (k !== this.apTok || this.posterBase || this.mv.ui.bookmarkManager?.prevEn === false) return; if (d) { this.video.poster = d; return; } } if (k === this.apTok && !this.posterBase) this.video.poster = initialsAva((bm && (bm.name || bm.title)) || (this.e && this.e.name) || u); } stopPoster() { clearTimeout(this.pt); this.pt = 0; this.pBusy = false; } canRefreshPoster() { return this.mv.isOpen() && !realHidden() && !this.pBusy; } async refreshPoster() { this.pBusy = true; const d = await loadFrame(frameBust(this.posterBase)); this.pBusy = false; if (this.parked && d) this.video.poster = d; } prevOk() { return !!prevSpd() && this.mv.ui.bookmarkManager?.prevEn !== false; } kickPoster() { this.stopPoster(); if (!this.parked || !this.posterBase || !this.prevOk()) return; const run = async () => { this.pt = 0; if (!this.parked || !this.posterBase || !this.prevOk()) return; if (this.canRefreshPoster()) await this.refreshPoster(); if (this.parked && this.prevOk() && !this.pt) this.pt = setTimeout(run, prevSpd()); }; run(); } inPip() { return !!this.pip; } pipKeep(on) { this.mv.pane?.classList.toggle('keep', on); this.mv.ui.bookmarkManager?.overlay?.classList.toggle('pipkeep', on); } setWant(on) { if (this.want === on) return; this.want = on; this.mv.saveOn(); } idle() { this.setWant(false); this.setPoster(); this.stop(); this.endUi(); this.parked = true; this.say('\u25B6'); this.msg.classList.add('park'); this.el.classList.remove('bad'); this.kickPoster(); } shotUrl(src) { if (this.e.t === 'url') return ''; const site = urlSite(this.e.url); if (site === 'cb') { const slug = modelSlug(this.e.url); return slug ? cbRiUrl(slug) : ''; } const m = site === 'bc' && src && String(src.url || '').match(/^https:\/\/live-edge([\w-]*)\.bcvcdn\.com\/hls\/(stream_[^/]+)\//i); return m ? `https://mobile-edge${m[1]}.bcvcdn.com/${m[2]}.jpg` : ''; } async liveShot(id, src) { if (this.mv.ui.bookmarkManager?.prevEn === false) return; const u = this.shotUrl(src); if (!u) return; const d = await loadFrame(frameBust(u)); if (d && id === this.gen && !this.played) this.video.poster = d; } async resolveSource() { try { return { src: await mvResolve(this.e), err: '' }; } catch (x) { return { src: null, err: String((x && x.message) || x).slice(0, 60) }; } } async start() { this.stop(); const id = ++this.gen; this.parked = false; this.userPause = false; this.sw = Date.now(); this.lazy = false; this.stopPoster(); this.msg.classList.remove('park'); if (this.act && !this.ti) { this.showUi(); this.ti = setInterval(() => this.tickUi(), 250); } this.say(L().mv_load); const vk = this.vkey = mvVaultKey(this.e); const tryWarm = !!vk && !this.skipWarm && !mvIsSelf(this.e); this.skipWarm = false; if (vk && tryWarm) await mvVault.ready(); if (id !== this.gen) return; const warm = tryWarm ? mvVault.peek(vk) : null; this.usedWarm = !!(warm && warm.src && warm.src.url); this.liveShot(id, this.usedWarm ? warm.src : null); const { src, err } = this.usedWarm ? { src: warm.src, err: '' } : await this.resolveSource(); if (id !== this.gen) return; this.srcInfo = src; if (!this.usedWarm && src && this.e.t !== 'url' && urlSite(this.e.url) === 'bc') this.liveShot(id, src); this.dbg = [src && src.dbg, err].filter(Boolean).join(' '); if (!src || !src.url) { this.why = 'E1'; return this.ended(id); } const player = this.player = new HlsKit.HlsPlayer({ video: this.video, url: src.url, maxHeight: this.maxH(), keep: mvKeep(), hint: warm && warm.variants ? { variants: warm.variants, audio: warm.audio } : null, onOpened: rec => { if (vk && src.url) mvVault.keep(vk, { src: { url: src.url, ref: src.ref }, variants: rec.variants, audio: rec.audio }); }, fetchBin: (u, r) => mvGet(u, src.ref, r), onMuted: () => { this.um = true; this.applyAudio(); }, onEnd: out => { this.why = `E2 ${mvErr(out && out.error)}`; this.ended(id); } }); player.start().catch(x => { this.why = `E2 ${mvErr(x)}`; this.ended(id); }); this.abrStart(); this.wdStart(); } wdStart() { clearInterval(this.wd); const v = this.video; let lt = -1, at = Date.now(); this.played = false; this.wdT0 = Date.now(); this.wd = setInterval(() => { const p = this.player, now = Date.now(); if (!p || p.dead) { lt = v.currentTime; at = now; return; } if (!this.played) { if (now - this.wdT0 > 25000) { this.why = 'WD0'; this.start(); } return; } if (v.paused && mvBg()) this.bgResume(); if (v.paused || p.shift || v.currentTime !== lt) { lt = v.currentTime; at = now; return; } if (now - at > 10000) { this.why = 'WD'; this.tries = 0; this.start(); } }, 2000); } abrOn() { return !this.q && mvAbr(); } maxH() { if (this.q) return this.q; if (!mvAbr()) return Infinity; const a = [this.abrH || mvAbrLast(), MvNet.cap()].filter(x => x > 0); return a.length ? Math.min(...a) : Infinity; } ladder() { const st = this.player && this.player.stats; return st && st.variants ? [...new Set(st.variants.map(x => x.h).filter(Boolean))].sort((a, b) => b - a) : []; } curH() { const st = this.player && this.player.stats; return st && st.variant ? st.variant.h || 0 : 0; } abrReady(now) { const p = this.player, v = this.video; return !!p && !p.dead && p.started && this.abrOn() && !v.paused && !v.seeking && !p.shift && !realHidden() && now - this.abrAt >= 8000 && now - p.tStart >= 6000; } abrStart() { this.abrStop(); this.abrAt = Date.now(); this.okSince = 0; this.lowN = 0; this.waitAt = 0; this.stalls = []; this.abrTimer = setInterval(() => this.abrTick(), 1000); } abrStop() { clearInterval(this.abrTimer); this.abrTimer = 0; } abrTick() { const now = Date.now(); if (!this.abrReady(now)) { this.okSince = 0; this.lowN = 0; return; } if (now - this.abrAt > 300000 && this.abrBack > ABR_BACK0) this.abrBack = ABR_BACK0; const v = this.video, ahead = this.player._ahead(); if (v.readyState < 3 && this.waitAt && now - this.waitAt > 6000) { this.waitAt = now; this.abrStall(true); return; } this.lowN = ahead < ABR_LOW ? this.lowN + 1 : 0; if (this.lowN >= 3) { this.lowN = 0; if (this.abrStall(false)) return; } if (v.readyState < 3 || ahead < ABR_LOW) { this.okSince = 0; return; } if (!this.okSince) this.okSince = now; if (now - this.okSince >= ABR_UP_MS) this.abrUp(now); } abrStall(hard) { const now = Date.now(); this.okSince = 0; if (!hard && now - this.lastStall < 3000) return false; this.lastStall = now; this.stalls = this.stalls.filter(t => now - t < 30000); this.stalls.push(now); if (hard) this.stalls.push(now); return this.stalls.length >= 2 ? this.abrDown() : false; } abrDown() { const cur = this.curH(), lower = this.ladder().filter(h => h < cur)[0]; if (!cur || !lower) return false; this.abrBlock[cur] = Date.now() + this.abrBack; this.abrBack = Math.min(this.abrBack * 2, 600000); this.abrSwitch(lower); return true; } abrUp(now) { const cur = this.curH(), up = this.ladder().filter(h => h > cur).pop(), cap = MvNet.cap(); if (cur && now - this.savedAt > 60000) { this.savedAt = now; Store.set('bc_mv_abr_h', { h: cur, t: now }); } if (!cur || !up || (cap && up > cap) || (this.abrBlock[up] || 0) > now) return; this.abrSwitch(up); } abrNet() { if (!this.abrOn() || !this.player) return; const cap = MvNet.cap(), cur = this.curH(), hs = this.ladder(); if (!cap || !cur || cur <= cap || hs.length < 2) return; const to = hs.filter(h => h <= cap)[0] || hs[hs.length - 1]; if (to < cur) this.abrSwitch(to); } abrSwitch(h) { this.abrH = h; this.stalls = []; this.okSince = 0; Store.set('bc_mv_abr_h', { h, t: Date.now() }); this.start(); } abrOff() { if (!this.abrH) return; this.abrH = 0; if (!this.q && this.player) this.start(); } ended(id) { if (id !== this.gen) return; if (this.usedWarm && !this.played) { this.usedWarm = false; this.skipWarm = true; mvVault.forget(this.vkey); return this.start(); } if (++this.tries <= 3 || this.inPip() || (mvBg() && this.want)) { this.say(L().mv_load); this.rt = setTimeout(() => { if (id === this.gen) this.start(); }, this.tries === 1 ? 300 : Math.min(5000 * (this.tries - 1), 15000)); } else this.say(`${L().mv_off} · ${L().mv_retry}\n${this.why || 'E0'}${this.dbg ? `\n${this.dbg}` : ''}`, true); } stop() { this.gen++; clearTimeout(this.rt); clearInterval(this.wd); this.abrStop(); if (this.player) { this.player.stop(); this.player = null; } } destroy() { this.lazy = false; this.setWant(false); if (this.inPip()) { this.pip = false; this.pipKeep(false); } try { if (this.recS) this.recS.stop(); } catch (e) { dbg(e, 'MvTile.destroy'); } this.stop(); this.stopPoster(); this.endUi(); clearInterval(this.lblTimer); this.el.remove(); } } class MultiView { constructor(ui) { this.ui = ui; this.tiles = []; this.act = null; this.pane = null; this.loaded = false; this.dismissed = new Set(); this._chk = 0; onPageEvent('visibilitychange', () => { if (!realHidden()) this.tiles.forEach(t => t.bgResume()); }); setInterval(() => { if (this.pane && this.autoOn() && this.isOpen() && !realHidden()) this.autoFill(); }, 30000); } isOpen() { const bm = this.ui.bookmarkManager; return !!bm && bm.isOpen() && bm.mode === 'mv'; } open() { this.ui.bookmarkManager?.open('mv'); } show() { if (!this.pane) this.build(); this.labels(); if (!this.loaded) { const wasOn = Store.get('bc_mv_on', []); this.loaded = true; this.restoring = true; const saved = Store.get('bc_mv', []); (Array.isArray(saved) ? saved : []).filter(e => isObj(e) && isStr(e.url) && (e.t === 'bm' || e.t === 'url')).slice(0, MV_MAX).forEach(e => this.add({ t: e.t, name: String(e.name || ''), url: e.url }, true)); this.restoring = false; const was = new Set(Array.isArray(wasOn) ? wasOn : []); this.tiles.forEach(t => { const on = mvBg() && was.has(this.key(t.e)) && !Store.get('bc_sleep_lock', 0); t.idle(); if (on && t !== this.act) t.lazy = true; }); } if (this.autoOn()) { this.autoFill(); const bm = this.ui.bookmarkManager; if (bm) bm.checkStatuses().then(() => { if (this.isOpen() && this.autoOn()) this.autoFill(); }).catch(() => {}); } this.layout(); requestAnimationFrame(() => { this.layout(); this.lazyCheck(); }); setTimeout(() => this.lazyCheck(), 350); } paintFlt() { if (!this.fltSeg) return; const l = L(); this.fltSeg.replaceChildren(...MV_FLT.map(([key, def, label, tip]) => bmTbtn( escHTML(l[label]), mvFltOn(key, def), () => { Store.set(key, !mvFltOn(key, def)); this.paintFlt(); this.layout(); if (this.addPanel.classList.contains('open')) this.fillAdd(); if (this.autoOn()) this.autoFill(); const bm = this.ui.bookmarkManager; if (bm) bm.checkStatuses().then(() => { this.layout(); if (this.autoOn()) this.autoFill(); }).catch(() => {}); }, l[tip] ))); } lazyCheck() { if (!this.isOpen() || realHidden()) return; const H = window.innerHeight; for (const t of this.tiles) { if (!t.lazy) continue; const r = t.el.getBoundingClientRect(); if (!r.height) continue; const vis = Math.min(r.bottom, H) - Math.max(r.top, 0); if (vis >= Math.min(r.height, H) * 0.4) { t.lazy = false; t.start(); } } } dropBmKey(k) { const same = e => e && e.t === 'bm' && isStr(e.url) && bmKey(e.url) === k; if (!this.loaded) { const saved = Store.get('bc_mv', []); if (Array.isArray(saved) && saved.some(same)) Store.set('bc_mv', saved.filter(e => !same(e))); return; } this.tiles.filter(t => same(t.e)).forEach(t => this.remove(t)); } autoOn() { return Store.get('bc_mv_auto', true) !== false; } mvPass(b) { const st = b.status ?? 0; if (st === 0 && mvFltOn(MV_FLT[0][0], MV_FLT[0][1])) return false; const lk = bmStatusKey(b); return !MV_FLT.some(([key, def,,, k]) => k && k === lk && mvFltOn(key, def)); } autoFill() { if (!this.pane || !this.autoOn()) return; this.layout(); this.restoring = true; const fresh = []; try { for (const b of (AppState.bookmarks || [])) { if (this.tiles.length >= MV_MAX) break; if (!isObj(b) || !isStr(b.url) || !isSafeUrl(b.url) || !((b.status ?? 0) > 0) || !this.mvPass(b)) continue; const e = { t: 'bm', name: String(b.name), url: b.url }; if (this.dismissed.has(this.key(e))) continue; const t = this.add(e, true); if (t) fresh.push(t); } } finally { this.restoring = false; } fresh.forEach(t => t.idle()); if (fresh.length) this.save(); } setAuto(on) { Store.set('bc_mv_auto', !!on); if (on) { this.dismissed.clear(); this.autoFill(); const bm = this.ui.bookmarkManager; if (bm) bm.checkStatuses().then(() => { if (this.pane && this.autoOn()) this.autoFill(); }).catch(() => {}); } if (this.pane) this.labels(); } suspend() { if (mvBg()) return; this.tiles.forEach(t => { if (!t.inPip()) t.idle(); }); } build() { const st = document.createElement('style'); st.textContent = MV_CSS; this.ui.shadow.appendChild(st); const onScroll = () => { if (this._chk) return; this._chk = requestAnimationFrame(() => { this._chk = 0; this.lazyCheck(); }); }; this.ui.shadow.addEventListener('scroll', onScroll, true); window.addEventListener('scroll', onScroll, { passive: true, capture: true }); const pane = this.pane = mk('div', 'mv-pane'); const bar = mk('div', 'mv-bar'); this.btnAdd = mk('button', 'bm-tbtn on'); this.btnAdd.onclick = () => this.toggleAdd(); this.btnCols = mk('button', 'bm-hbtn bm-colsbtn'); this.btnCols.onclick = () => { Store.set(COLS_KEY, colsGet() === 2 ? 1 : 2); this.layout(); }; this.btnDvr = mk('button', 'bm-tbtn'); this.btnDvr.onclick = () => this.cycleDvr(); this.btnPrev = mk('button', 'bm-hbtn bm-prevbtn'); this.btnPrev.onclick = () => { const bm = this.ui.bookmarkManager; if (!bm) return; bm.prevEn = !bm.prevEn; Store.set('bc_previews', bm.prevEn); bm.updatePrevUI?.(); bm.stopPreviews?.(); this.syncPrev(); }; this.btnSpd = mk('div', 'bm-spdseg'); this.spdBtns = PREV_SPD_SEG.map(v => { const b = mk('button', 'bm-spdopt'); b.type = 'button'; b.textContent = prevSpdLabel(v); b.onclick = () => { Store.set('bc_prev_spd', v); this.labels(); this.tiles.forEach(t => t.kickPoster()); const bm = this.ui.bookmarkManager; bm?.updatePrevUI?.(); bm?.stopPreviews?.(); }; this.btnSpd.append(b); return [v, b]; }); this.btnClr = mk('button', 'bm-tbtn mv-clr'); this.btnClr.innerHTML = MV_IC.close; this.btnClr.onclick = () => askConfirm(this.ui, mvT('mv_clear_q'), () => this.clear()); this.swAbr = mk('button', 'bm-sw'); this.swAbr.onclick = () => this.toggleAbr(); this.swAuto = mk('button', 'bm-sw'); this.swAuto.onclick = () => this.setAuto(!this.autoOn()); this.swBg = mk('button', 'bm-sw'); this.swBg.onclick = () => { Store.set('bc_mv_bg', !mvBg()); this.labels(); }; this.btnGear = mk('button', 'bm-hbtn bm-gear'); this.btnGear.innerHTML = icons.gear; this.setPanel = mk('div', 'mv-setp'); const setRow = (k, ctl) => { const r = mkSetRow('', ctl); this[k] = r.firstChild; return r; }; this.setPanel.append(setRow('lblAuto', this.swAuto), setRow('lblBg', this.swBg), setRow('lblDvr', this.btnDvr), setRow('lblAbr', this.swAbr), (() => { const r = setRow('lblSpd', this.btnSpd); r.classList.add('bm-setrow-spd'); return r; })()); bindGear(this.btnGear, this.setPanel); bar.append(this.btnAdd, this.btnPrev, this.btnCols, this.btnGear); MvNet.watch(() => this.tiles.forEach(t => t.abrNet())); const stage = this.stage = mk('div', 'mv-stage'); this.grid = mk('div', 'mv-grid'); this.empty = mk('div', 'mv-empty'); this.emptyTxt = mk('div'); this.emptyBtn = mk('button', 'bm-tbtn on'); this.emptyBtn.onclick = () => this.toggleAdd(true); this.empty.append(this.emptyTxt, this.emptyBtn); this.addPanel = mk('div', 'mv-add'); stage.append(this.grid, this.empty, this.addPanel); this.fltSeg = mk('div', 'bm-seg mv-seg'); pane.append(bar, this.fltSeg, this.setPanel, stage); this.ui.bookmarkManager.modal.appendChild(pane); } labels() { this.paintFlt(); const l = L(); this.btnAdd.innerHTML = `${MV_IC.add} ${escHTML(l.mv_add)}`; this.emptyBtn.innerHTML = `${MV_IC.add} ${escHTML(l.mv_add)}`; this.emptyTxt.textContent = l.mv_hint; this.btnCols.title = l.st_cols || l.mv_cols; this.btnDvr.innerHTML = `${MV_IC.rew}<span>${escHTML(this.dvrLabel())}</span>`; this.btnDvr.title = mvT('mv_dvr'); this.swAbr.classList.toggle('on', mvAbr()); this.swAuto.classList.toggle('on', this.autoOn()); this.swBg.classList.toggle('on', mvBg()); this.swBg.title = mvT('mv_bg_t'); this.lblAuto.textContent = mvT('mv_auto_all'); this.lblBg.textContent = mvT('mv_bg'); this.swAbr.title = mvT('mv_abr_t'); this.lblDvr.textContent = mvT('mv_dvr'); this.lblAbr.textContent = mvT('mv_abr_row'); this.lblSpd.textContent = l.st_prev_spd; this.btnGear.title = l.settings; this.btnClr.title = l.mv_close; this.btnSpd.title = prevSpdTitle(); const pe = this.ui.bookmarkManager?.prevEn !== false; this.btnPrev.classList.toggle('on', pe); this.btnPrev.innerHTML = pe ? icons.eye : icons.eyeOff; this.btnPrev.title = l.st_prev; this.btnSpd.style.opacity = pe ? '' : '.4'; this.btnSpd.style.pointerEvents = pe ? '' : 'none'; this.spdBtns.forEach(([v, b]) => { b.classList.toggle('on', prevSpd() === v); b.disabled = !pe; }); } syncPrev() { if (!this.pane) return; this.labels(); this.tiles.forEach(t => { t.setPoster(); t.kickPoster(); }); } toggleAbr() { const on = !mvAbr(); Store.set('bc_mv_abr', on); if (!on) this.tiles.forEach(t => t.abrOff()); this.labels(); } dvrLabel() { const v = mvDvr(); return v ? `${v / 60} ${mvT('mv_min_u')}` : mvT('mv_dvr_off'); } cycleDvr() { const nx = MV_DVR_OPTS[(MV_DVR_OPTS.indexOf(mvDvr()) + 1) % MV_DVR_OPTS.length]; Store.set('bc_mv_dvr', nx); this.tiles.forEach(t => { t.applyDvr(); if (t.player) t.player.o.keep = mvKeep(); if (!nx && t.act) t.goLive(); }); this.labels(); } tilePass(t) { if (t.e.t !== 'bm') return true; const b = (AppState.bookmarks || []).find(x => isObj(x) && x.url === t.e.url); return !b || this.mvPass(b); } layout() { if (!this.pane) return; let shown = 0; this.tiles.forEach(t => { const ok = this.tilePass(t); if (ok) shown++; else if (t.el.style.display !== 'none') { try { t.idle(); } catch (e) { dbg(e, 'MultiView.layout'); } } t.el.style.display = ok ? '' : 'none'; }); const n = shown; const c = colsGet(); this.btnCols.innerHTML = COLS_ICONS[colsGet() - 1]; this.grid.style.gridTemplateColumns = `repeat(${c},minmax(0,1fr))`; this.grid.classList.toggle('multi', c > 1); this.empty.style.display = n ? 'none' : 'flex'; this.grid.style.display = n ? 'grid' : 'none'; this.btnClr.style.display = n ? '' : 'none'; } key(e) { return `${e.t}|${e.url}`; } add(e, quiet) { if (this.tiles.length >= MV_MAX) { this.btnAdd.textContent = L().mv_full; setTimeout(() => this.labels(), 2000); return null; } if (this.tiles.some(t => this.key(t.e) === this.key(e))) return null; const nm = String(e.name).toLowerCase(); if (this.tiles.some(t => t.e.t !== e.t && String(t.e.name).toLowerCase() === nm)) return null; if (!this.restoring) this.dismissed.delete(this.key(e)); const t = new MvTile(this, e); this.tiles.push(t); this.grid.append(t.el); this.layout(); if (this.isOpen() && !this.restoring) { if (!this.act) this.select(t); t.start(); } if (!quiet) this.save(); if (this.addPanel.classList.contains('open')) this.fillAdd(); return t; } remove(t) { this.dismissed.add(this.key(t.e)); t.destroy(); this.tiles = this.tiles.filter(x => x !== t); if (this.act === t) this.act = null; this.layout(); this.save(); if (this.addPanel.classList.contains('open')) this.fillAdd(); } clear() { this.tiles.forEach(t => this.dismissed.add(this.key(t.e))); this.tiles.forEach(t => t.destroy()); this.tiles = []; this.act = null; this.layout(); this.save(); } select(t) { const prev = this.act; this.act = prev === t ? null : t; if (prev) prev.setActive(false); if (this.act) this.act.setActive(true); } save() { Store.set('bc_mv', this.tiles.map(t => ({ t: t.e.t, name: t.e.name, url: t.e.url }))); } saveOn() { if (!this.loaded || this.restoring || Store.get('bc_sleep_lock', 0)) return; Store.set('bc_mv_on', this.tiles.filter(t => t.want).map(t => this.key(t.e))); } bootResume() { const on = Store.get('bc_mv_on', []); if (!mvBg() || !Array.isArray(on) || !on.length || Store.get('bc_sleep_lock', 0) || this.loaded) return; try { this.show(); } catch (e) { dbg(e, 'MultiView.bootResume'); } } toggleAdd(force) { const on = force === undefined ? !this.addPanel.classList.contains('open') : force; this.addPanel.classList.toggle('open', on); this.stage.classList.toggle('adding', on); if (on) this.fillAdd(); } fillAdd() { const l = L(), p = this.addPanel, keep = p.querySelector('.mv-f')?.value || ''; const finish = t => { if (t) this.toggleAdd(false); }; const sel = new Map(); const back = mk('button', 'bm-tbtn'); back.textContent = `← ${l.mv_back}`; const hd = mk('div', 'mv-hd'); const gob = mk('button', 'bm-tbtn on mv-gob'); gob.style.display = 'none'; hd.append(back, textEl('', l.mv_add, 'span'), gob); const flt = mk('input', 'mv-inp mv-f'); flt.placeholder = l.mv_search; flt.value = keep; const list = mk('div', 'mv-list'); const paintSel = () => { list.classList.toggle('selmode', sel.size > 0); gob.style.display = sel.size ? '' : 'none'; gob.textContent = `${l.mv_go} (${sel.size})`; }; back.onclick = () => { if (!sel.size) return this.toggleAdd(false); sel.clear(); list.querySelectorAll('.mv-it.sel').forEach(x => x.classList.remove('sel')); paintSel(); }; const toggleRow = (it, b) => { if (sel.has(b.url)) sel.delete(b.url); else { if (this.tiles.length + sel.size >= MV_MAX) { gob.textContent = l.mv_full; setTimeout(paintSel, 1500); return; } sel.set(b.url, b); } it.classList.toggle('sel', sel.has(b.url)); paintSel(); }; gob.onclick = () => { const arr = [...sel.values()]; this.toggleAdd(false); for (const b of arr) if (!this.add({ t: 'bm', name: String(b.name), url: b.url }, true)) break; this.save(); }; const draw = () => { const q = flt.value.trim().toLowerCase(); const have = new Set(this.tiles.map(t => this.key(t.e))); const rows = []; const cur = getStreamUrl(); const curName = String(currentSite.getName() || ''); if (cur && !q && !(curName && this.tiles.some(t => String(t.e.name).toLowerCase() === curName.toLowerCase()))) { const it = mk('div', 'mv-it cur'); it.append(textEl('', `${l.mv_cur}: ${curName}`, 'b')); it.onclick = () => finish(this.add(curName && isSafeUrl(location.origin + location.pathname) ? { t: 'bm', name: curName, url: location.origin + location.pathname } : { t: 'url', name: curName || 'LIVE', url: cur })); rows.push(it); } const items = (AppState.bookmarks || []).filter(b => isObj(b) && this.mvPass(b) && (!q || String(b.name).toLowerCase().includes(q))).sort((a, b) => bmViews(b) - bmViews(a)); items.forEach(b => { const it = mk('div', `mv-it${have.has(this.key({ t: 'bm', url: b.url })) ? ' dis' : ''}${sel.has(b.url) ? ' sel' : ''}`); const im = document.createElement('img'); im.referrerPolicy = 'no-referrer'; im._bRef = b; im.onerror = () => { if (!tryGm(im, b.frame, () => frameFail(im))) frameFail(im); }; if (b.frame) im.src = b.frame; else setTimeout(() => frameFail(im), 0); it.append(mk('span', 'mv-ck'), im, textEl('', b.name, 'b'), textEl('', `${urlSite(b.url).toUpperCase()}${bmViews(b) ? ` · ${fmtCount(bmViews(b))}` : ''}`, 'i')); let timer = 0, sx = 0, sy = 0; const stop = () => { clearTimeout(timer); timer = 0; }; it.onpointerdown = ev => { if (ev.pointerType === 'mouse' && ev.button !== 0) return; it._lp = false; sx = ev.clientX; sy = ev.clientY; stop(); timer = setTimeout(() => { timer = 0; it._lp = true; try { if (navigator.vibrate) navigator.vibrate(15); } catch (e) { dbg(e, 'fillAdd.draw'); } toggleRow(it, b); }, 450); }; it.onpointermove = ev => { if (timer && Math.hypot(ev.clientX - sx, ev.clientY - sy) > 10) stop(); }; it.onpointerup = stop; it.onpointercancel = stop; it.onpointerleave = stop; it.oncontextmenu = ev => ev.preventDefault(); it.onclick = () => { if (it._lp) { it._lp = false; return; } if (sel.size) return toggleRow(it, b); finish(this.add({ t: 'bm', name: String(b.name), url: b.url })); }; rows.push(it); }); list.replaceChildren(...(rows.length ? rows : [textEl('', l.mv_none)])); paintSel(); }; flt.oninput = draw; draw(); const inp = mk('input', 'mv-inp'); inp.placeholder = l.mv_url; const go = mk('button', 'bm-tbtn on'); go.textContent = l.mv_go; const submit = () => { const u = inp.value.trim(); if (!/^https?:\/\/\S+$/i.test(u)) return; let host = ''; try { host = new URL(u).hostname; } catch (e) { dbg(e, 'fillAdd.submit'); } finish(this.add({ t: 'url', name: host, url: u })); }; go.onclick = submit; inp.onkeydown = ev => { if (ev.key === 'Enter') submit(); }; const r2 = mk('div', 'mv-r2'); r2.append(inp, go); p.replaceChildren(hd, textEl('mv-sec', l.mv_bm_h), flt, list, textEl('mv-sec', l.mv_link_h), r2); } } const STATUS_RULES = [ [/"is(?:Full)?PrivateChat"\s*:\s*true/i, 3, 'st_private'], [/"isGroupChat"\s*:\s*true/i, 3, 'st_group'], [/"isVipShow"\s*:\s*true/i, 3, 'st_vip'], [/"isAway"\s*:\s*true/i, 2, 'st_away'], [/"showType"\s*:\s*"(?:private|full_?private|voyeur)"/i, 3, 'st_private'], [/"showType"\s*:\s*"group"/i, 3, 'st_group'], [/"showType"\s*:\s*"vip"/i, 3, 'st_vip'], [/__page_profile|"isOnline"\s*:\s*false|"online"\s*:\s*false|"isOffline"\s*:\s*true|"room_status"\s*:\s*["']offline["']|"isChatShowPaused"\s*:\s*true/i, 0], [/"isOnline"\s*:\s*true|"online"\s*:\s*true|"showType"\s*:\s*"public"|"isOffline"\s*:\s*false|"room_status"\s*:\s*["'](?:public|free|chat|group|private)["']/i, 1] ]; const STATUS_RULES_SITE = { sc: [ [/"status"\s*:\s*"(?:private|p2p|p2pVoice|virtualPrivate|spy)"/i, 3, 'st_private'], [/"isInPrivate"\s*:\s*true|"isInP2p"\s*:\s*true/i, 3, 'st_private'], [/"status"\s*:\s*"(?:group|groupShow|ticketShow)"|"isInGroupShow"\s*:\s*true/i, 3, 'st_group'], [/"status"\s*:\s*"public"|"isLive"\s*:\s*true|"isBroadcasting"\s*:\s*true/i, 1], [/"status"\s*:\s*"(?:off|offline|idle)"|"isLive"\s*:\s*false|"isOnline"\s*:\s*false/i, 0] ], cb: [ [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)(?:private|hidden|password protected)/i, 3, 'st_private'], [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)group/i, 3, 'st_group'], [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)away/i, 2, 'st_away'], [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)public/i, 1], [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)offline/i, 0], [/(?:"|\\u0022)hls_source(?:"|\\u0022)\s*:\s*(?:"|\\u0022)https?:/i, 1] ] }; const SC_JSON_STATUS = [ [/^(?:private|p2p|p2pVoice|virtualPrivate|spy)$/i, 3, 'st_private'], [/^(?:group|groupShow|ticketShow)$/i, 3, 'st_group'] ]; const scJsonUser = h => { try { const u = JSON.parse(h)?.user?.user; return u && typeof u.status === 'string' ? u : null; } catch { return null; } }; const scJsonStatus = h => { if (!h || h.charCodeAt(0) !== 123) return null; const u = scJsonUser(h); if (!u) return null; const hit = SC_JSON_STATUS.find(([re]) => re.test(u.status)); if (hit) return [hit[1], hit[2]]; return /^public$/i.test(u.status) && u.isLive !== false ? [1, ''] : [0, '']; }; const parseStatus = (h, site) => { if (site === 'sc') { const j = scJsonStatus(h); if (j) return j; } const rule = h && (STATUS_RULES_SITE[site] || STATUS_RULES).find(([re]) => re.test(h)); if (!rule) return [0, '', false]; const [, st, lk] = rule; return [st, lk || '', true]; }; const decodeJson = s => { try { return JSON.parse(`"${s}"`); } catch { return s; } }; const jsonField = (h, re) => { const m = h.match(re); return m?.[1] ? decodeJson(m[1]) : ''; }; const TOPIC_RE = /"currentTopic"\s*:\s*"((?:[^"\\]|\\.)*)"/i; const FRAME_RE = /"frame"\s*:\s*"((?:[^"\\]|\\.)*)"/i; const htmlDec = x => { try { return new DOMParser().parseFromString(x, 'text/html').documentElement.textContent || ''; } catch { return x; } }; const metaContent = (h, prop) => { const m = h.match(new RegExp(`(?:property|name)=["']${prop}["'][^>]*?content=["']([^"']*)["']`, 'i')) || h.match(new RegExp(`content=["']([^"']*)["'][^>]*?(?:property|name)=["']${prop}["']`, 'i')); return m ? htmlDec(m[1]) : ''; }; const intField = (h, re) => { const m = h.match(re); return m ? +m[1] : null; }; const siteProfile = { cb: (h, status, slug) => { if (!status) return { topic: '', frame: '' }; const d = cbDossier(h), dv = d && [d.num_users, d.num_viewers, d.viewer_count, d.viewers_count].find(x => Number.isFinite(+x) && x !== null && x !== ''); const v = dv != null ? +dv : intField(h, /(?:"|\\u0022)(?:num_users|num_viewers|viewer_count|viewers_count)(?:"|\\u0022)\s*:\s*(\d+)/i); const dt = d && [d.room_title, d.room_subject, d.subject].find(x => isStr(x) && x.trim()); return { topic: String(dt || jsonField(h, /"(?:room_title|room_subject|subject)"\s*:\s*"((?:[^"\\]|\\.)*)"/i) || metaContent(h, 'og:description')).trim(), frame: slug ? cbFrameUrl(slug) : '', ...(v != null ? { viewers: v } : {}) }; }, sc: (h, status) => { if (!status) return { topic: '', frame: '' }; const v = intField(h, /"(?:viewersCount|usersCount|viewers)"\s*:\s*(\d+)/i); const fr = (jsonField(h, /"(?:previewUrlThumbBig|previewUrlThumbSmall|previewUrl)"\s*:\s*"((?:[^"\\]|\\.)*)"/i) || metaContent(h, 'og:image')).replace(/^\/\//, 'https://'); return { topic: (jsonField(h, /"(?:topic|goalMessage)"\s*:\s*"((?:[^"\\]|\\.)*)"/i) || metaContent(h, 'og:description')).trim(), frame: fr.startsWith('https://') ? fr : '', ...(v != null ? { viewers: v } : {}) }; } }; const stripTags = s => String(s).replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, ' ').trim(); const ptIcoUrl = p => (isStr(p) && /^[a-z0-9_\/-]{1,60}$/i.test(p) ? `https://i.bgicdn.com/images/tags/${p}/18/2x/img.png` : ''); const ptIcoMap = () => { const m = new Map(); for (const b of AppState.bookmarks) { if (!isObj(b) || !Array.isArray(b.ptags) || !Array.isArray(b.ptic)) continue; b.ptags.forEach((x, i) => { const u = ptIcoUrl(b.ptic[i]), k = String(x).toLowerCase(); if (u && !m.has(k)) m.set(k, u); }); } return m; }; const ptImg = (u, cls) => (u ? `<img class="${cls}" src="${escHTML(u)}" alt="" decoding="sync" referrerpolicy="no-referrer">` : ''); const pilgSection = (h, cls) => { const i = h.indexOf(cls); if (i < 0) return ''; const j = h.indexOf('pilg_section', i + cls.length); return h.slice(i, j < 0 ? i + 12000 : j); }; const parsePilgTags = ts => { const tags = [], icos = []; const add = raw => { const x = stripTags(raw); if (x && x.length <= 40 && tags.length < 30 && !tags.includes(x)) { tags.push(x); const im = String(raw).match(/images\/tags\/([a-z0-9_\/-]{1,60}?)\/\d+\/(?:\d+x\/)?img\./i); icos.push(im ? im[1] : ''); } }; for (const m of ts.matchAll(/<a\b[^>]*\/tags\/[^"'\/?#>\s]+[^>]*>([\s\S]*?)<\/a>/gi)) add(m[1]); if (!tags.length) for (const m of ts.matchAll(/class=["']stl_item["'][^>]*>([\s\S]*?)<\/div>/gi)) add(m[1]); return tags.length ? { ptags: tags, ptic: icos.some(Boolean) ? icos : [] } : null; }; const parsePilgRow = row => { const rng = []; for (const x of row.matchAll(/pilgs_badge[^>]*>([^<]*)</g)) { const m = x[1].match(/(\d{1,2}):(\d{2})\s*[-\u2013\u2014]\s*(\d{1,2}):(\d{2})/); if (m && !(+m[1] === +m[3] && m[2] === m[4])) rng.push(`${m[1].padStart(2, '0')}:${m[2]}-${m[3].padStart(2, '0')}:${m[4]}`); } return rng.join(','); }; const parsePilgSchedule = ss => { let hi = -1; const days = ss.split('pilg_shelf').slice(1).map((row, i) => { if (hi < 0 && row.indexOf('__highlight') >= 0) hi = i; return parsePilgRow(row); }); if (days.length !== 7) return null; const sh = hi < 0 ? 0 : (((new Date().getDay() + 6) % 7) - hi + 7) % 7; return days.map((_, i) => days[(i - sh + 7) % 7]); }; const bcProfileUrl = b => { const slug = modelSlug(b.url); if (urlSite(b.url) !== 'bc' || !slug) return ''; try { return `${new URL(b.url).origin}/profile/${encodeURIComponent(slug)}/`; } catch { return ''; } }; const tagText = item => { if (isStr(item)) return item; return item && typeof item === 'object' ? (item.name || item.title || item.tag || item.value || '') : ''; }; const collectProfileTags = raw => { const tags = [], icos = []; for (const item of raw) { const tag = stripTags(String(tagText(item))).trim(); if (!tag || tag.length > 40 || tags.includes(tag) || tags.length >= 30) continue; tags.push(tag); icos.push(item && typeof item === 'object' && isStr(item.icon) ? item.icon : ''); } return { tags, icos }; }; const parseBcJsonTags = h => { const re = /"(?:modelTags|profileTags|tags)"\s*:\s*(\[(?:[^\[\]]|\[[^\]]*\])*\])/ig; for (const m of h.matchAll(re)) { try { const raw = JSON.parse(m[1]); if (!Array.isArray(raw)) continue; const { tags, icos } = collectProfileTags(raw); if (tags.length) return { ptags: tags, ...(icos.some(Boolean) ? { ptic: icos } : {}) }; } catch (e) { dbg(e, 'parseBcJsonTags'); } } return null; }; const isWeekSchedule = a => Array.isArray(a) && a.length === 7 && a.every(x => isStr(x) || x == null); const scheduleDays = a => a.map(x => { const hits = String(x || '').match(/\d{1,2}:\d{2}\s*[-–—]\s*\d{1,2}:\d{2}/g) || []; return hits.map(v => v.replace(/\s+/g, '').replace(/[–—]/g, '-')).join(','); }); const parseBcJsonSchedule = h => { const re = /"(?:modelSchedule|schedule)"\s*:\s*(\[(?:[^\[\]]|\[[^\]]*\])*\])/ig; for (const m of h.matchAll(re)) { try { const a = JSON.parse(m[1]); if (!isWeekSchedule(a)) continue; const days = scheduleDays(a); if (days.some(Boolean)) return days; } catch (e) { dbg(e, 'parseBcJsonSchedule'); } } return null; }; const pilgPart = (h, name) => (h.indexOf('pilg_section') >= 0 ? pilgSection(h, name) : ''); const bcViewers = h => { const m = h && h.match(/class=["'][^"']*\boc_count\b[^"']*["'][^>]*>\s*(\d[\d\s,.\u00a0]*)\s*</i); const n = m ? parseInt(m[1].replace(/\D/g, ''), 10) : NaN; return Number.isFinite(n) && n < 10000000 ? n : null; }; const splitBcd = (h, site) => { if (site !== 'bc' || !h) return [h, null]; const i = h.lastIndexOf(BCD_MARK); if (i < 0) return [h, null]; let bcd = null; try { bcd = JSON.parse(h.slice(i + BCD_MARK.length)); } catch (e) { dbg(e, 'splitBcd'); } return [h.slice(0, i), bcd]; }; const BC_FACT_SCOPES = ['chatShowStatusOptions', 'performerData', 'chatHost']; const BC_SCHEMA = { facts: { off: { keys: ['isOffline'], scopes: BC_FACT_SCOPES }, on: { keys: ['isOnline', 'online'], scopes: ['performerData', 'chatHost'] }, priv: { keys: ['isPrivateChat', 'isFullPrivateChat', 'isVoyeurChat', 'isInPrivate'], scopes: BC_FACT_SCOPES }, grp: { keys: ['isGroupChat'], scopes: BC_FACT_SCOPES }, vip: { keys: ['isVipShow'], scopes: BC_FACT_SCOPES }, away: { keys: ['isAway'], scopes: ['performerData', 'chatHost', 'chatShowStatusOptions'] } }, showType: { keys: ['showType', 'room_status', 'roomStatus'], scopes: ['chatHost', 'performerData'] }, topic: { keys: ['currentTopic', 'chatTopic', 'roomTopic'], scopes: ['chatTopicOptions'] }, frame: { keys: ['frame', 'frameUrl', 'previewFrame', 'previewUrl', 'livePreviewUrl'], scopes: ['initialVideoPoster', 'chatHost', 'performerData'] }, viewers: { keys: ['viewers', 'viewersCount', 'viewerCount', 'usersCount'], scopes: ['chatHost', 'performerData'] }, tags: { keys: ['modelTags', 'profileTags'], scopes: ['performerData', 'chatHost'] }, schedule: { keys: ['modelSchedule', 'schedule'], scopes: ['performerData', 'chatHost'] } }; const PD_WARN_RATIO = 0.3, PD_WARN_MIN = 5, PD_WINDOW = 600000, PD_STALE_MS = 1800000; const pdT = (k, v) => { const d = PD_T[String(AppState.lang).toUpperCase()] || PD_T.EN; return String(d[k] ?? PD_T.EN[k]).replace(/\{(\w+)\}/g, (m, x) => (x === 'copy' ? (d.copy ?? PD_T.EN.copy) : (v && v[x] != null ? v[x] : m))); }; const bcRoots = h => { const roots = []; if (!isStr(h) || !h) return roots; const c0 = h.charCodeAt(0); if (c0 === 123 || c0 === 91) { try { const j = JSON.parse(h); if (isObj(j)) roots.push(j); } catch (e) { dbg(e, 'bcRoots'); } } if (h.indexOf('<script') >= 0) { for (const m of h.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)) { if (roots.length >= 12) break; const body = m[2].trim(), c = body.charCodeAt(0); if (body.length < 2 || body.length > 3e6 || (c !== 123 && c !== 91) || !/json|data-type/i.test(m[1])) continue; try { const j = JSON.parse(body); if (isObj(j)) roots.push(j); } catch (e) { dbg(e, 'bcRoots'); } } } return roots; }; const jsonFind = (roots, names, pred, limit = 30000) => { const set = new Set(names.map(n => String(n).toLowerCase())); const q = roots.filter(isObj); let n = 0; for (let i = 0; i < q.length && n < limit; i++) { const o = q[i]; const ent = Array.isArray(o) ? o.map((v, k) => [String(k), v]) : Object.entries(o); for (const [k, v] of ent) { n++; if (set.has(k.toLowerCase()) && (!pred || pred(v))) return { value: v, key: k }; if (isObj(v)) q.push(v); } } return null; }; const bcFind = (roots, spec, pred, scopedOnly) => { if (!roots.length) return null; for (const sc of spec.scopes || []) { const s = jsonFind(roots, [sc], v => isObj(v) && !Array.isArray(v)); const r = s && jsonFind([s.value], spec.keys, pred); if (r) return { value: r.value, via: `json:${sc}` }; } if (scopedOnly) return null; const r = jsonFind(roots, spec.keys, pred); return r ? { value: r.value, via: 'json' } : null; }; const bcFact = (roots, spec) => { let seen = null; for (const k of spec.keys) { const r = bcFind(roots, { keys: [k], scopes: spec.scopes }, v => typeof v === 'boolean'); if (!r) continue; if (r.value) return { v: true, via: r.via }; seen = seen || { v: false, via: r.via }; } return seen; }; const BC_ROOM = new Map(); const bcListHint = slug => { const e = slug && BC_ROOM.get(String(slug).toLowerCase()); if (!e || Date.now() - e.t > 180000) return null; const r = String(e.room).toLowerCase(); if (/private|voyeur/.test(r)) return { st: 3, lk: 'st_private' }; if (/group/.test(r)) return { st: 3, lk: 'st_group' }; if (/vip/.test(r)) return { st: 3, lk: 'st_vip' }; if (/away/.test(r)) return { st: 2, lk: 'st_away' }; if (/^(?:public|free|chat)$/.test(r)) return { st: 1, lk: '' }; return null; }; const bcJsonStatus = roots => { if (!roots.length) return null; const F = {}; for (const k of Object.keys(BC_SCHEMA.facts)) F[k] = bcFact(roots, BC_SCHEMA.facts[k]); const sh = bcFind(roots, BC_SCHEMA.showType, isStr); const T = sh ? sh.value.trim().toLowerCase() : ''; const hit = f => F[f]?.v === true; const first = F.off || F.priv || F.grp || F.vip || F.away || F.on || sh; if (!first) return null; const via = first.via; if (hit('off')) return [0, '', via]; if (hit('priv') || /^(?:private|full_?private|voyeur|p2p)/.test(T)) return [3, 'st_private', via]; if (hit('grp') || /^group/.test(T)) return [3, 'st_group', via]; if (hit('vip') || T === 'vip') return [3, 'st_vip', via]; if (hit('away')) return [2, 'st_away', via]; if (F.off?.v === false || hit('on') || /^(?:public|free|chat)$/.test(T)) return [1, '', via]; if (F.on?.v === false || /^(?:offline|off)$/.test(T)) return [0, '', via]; return null; }; const resolveStatusEx = (h, site, bcd, roots, slug) => { if (site === 'bc') { if (bcd && typeof bcd.o === 'boolean') { let r = bcdStatus(bcd, h); if (bcd.o && h) { const a = parseStatus(h, site); if (a[0] === 2) r = a; } return { st: r[0], lk: r[1] || '', src: 'api:chat' }; } const j = bcJsonStatus(roots); if (j) return { st: j[0], lk: j[1], src: j[2] }; if (bcd && bcd.n === false) return { st: 0, lk: '', src: 'api:amf' }; if (bcd && bcd.a === true) return { st: 2, lk: 'st_away', src: 'api:amf' }; if (bcd && bcd.n === true) return { st: 1, lk: '', src: 'api:amf' }; const ls = bcListHint(slug); if (ls) return { ...ls, src: 'listing' }; } const r = parseStatus(h, site); if (r[2] === false) return { st: 0, lk: '', src: '', unk: true }; return { st: r[0], lk: r[1] || '', src: 'regex' }; }; const bcTopic = (h, bcd, roots) => { if (isStr(bcd?.t) && bcd.t.trim()) return [bcd.t.trim(), 'api:chat']; const j = bcFind(roots, BC_SCHEMA.topic, v => isStr(v) && !!v.trim()); if (j) return [j.value.trim(), j.via]; const r = jsonField(h, TOPIC_RE).trim(); return r ? [r, 'regex'] : ['', '']; }; const bcFrameNorm = s => String(s).replace(/\\\//g, '/').replace(/^\/\//, 'https://').replace(/^http:\/\//i, 'https://'); const bcFrame = (h, bcd, roots) => { const j = bcFind(roots, BC_SCHEMA.frame, v => isStr(v) && HTTP_RE.test(bcFrameNorm(v))); if (j) return [bcFrameNorm(j.value), j.via]; const rx = bcFrameNorm(jsonField(h, FRAME_RE) || jsonField(h, FRAME_ALT_RE)); if (HTTP_RE.test(rx)) return [rx, 'regex']; return isStr(bcd?.f) && HTTP_RE.test(bcd.f) ? [bcFrameNorm(bcd.f), 'api:amf'] : ['', '']; }; const bcViewersEx = (h, roots) => { const n = bcViewers(h); if (n != null) return [n, 'html:oc_count']; const j = bcFind(roots, BC_SCHEMA.viewers, v => (typeof v === 'number' || (isStr(v) && v.trim() !== '')) && Number.isFinite(+v) && +v >= 0 && +v < 1e7, true); return j ? [Math.round(+j.value), j.via] : [null, '']; }; const bcExtraSrc = (h, roots) => { const out = {}, src = { tags: '', sch: '' }; if (!h) return { out, src }; const ts = pilgPart(h, 'pilg_section __tags'); let tg = ts ? parsePilgTags(ts) : null; if (tg) src.tags = 'html:pilg'; if (!tg) { const j = bcFind(roots, BC_SCHEMA.tags, a => Array.isArray(a) && collectProfileTags(a).tags.length > 0); if (j) { const { tags, icos } = collectProfileTags(j.value); tg = { ptags: tags, ...(icos.some(Boolean) ? { ptic: icos } : {}) }; src.tags = j.via; } } if (!tg) { tg = parseBcJsonTags(h); if (tg) src.tags = 'regex'; } if (tg) Object.assign(out, tg); const ss = pilgPart(h, 'pilg_section __schedule'); let sch = ss ? parsePilgSchedule(ss) : null; if (sch) src.sch = 'html:pilg'; if (!sch) { const j = bcFind(roots, BC_SCHEMA.schedule, a => isWeekSchedule(a) && scheduleDays(a).some(Boolean)); if (j) { sch = scheduleDays(j.value); src.sch = j.via; } } if (!sch) { sch = parseBcJsonSchedule(h); if (sch) src.sch = 'regex'; } if (sch) out.sch = sch; return { out, src }; }; const parseBcExtra = h => bcExtraSrc(h, bcRoots(h)).out; const pdToast = msg => { try { const d = document.createElement('div'); d.textContent = msg; d.style.cssText = 'all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%);max-width:min(560px,92vw);z-index:2147483647;background:#3a2410;color:#ffd9a8;border:1px solid #c77d2a;border-radius:10px;padding:10px 14px;font:13px/1.35 system-ui,sans-serif;cursor:pointer;box-shadow:0 6px 24px rgba(0,0,0,.5)'; d.onclick = () => { d.remove(); ParserReport.open(); }; (document.body || document.documentElement).appendChild(d); setTimeout(() => d.remove(), 15000); } catch (e) { dbg(e, 'pdToast'); } }; const ParserDiag = { recs: new Map(), warned: false, flag: null, rec(site, slug, src, unk, status, sig) { try { if (!slug) return; const k = `${site}|${String(slug).toLowerCase()}`; this.recs.delete(k); this.recs.set(k, { t: Date.now(), site, slug, st: status, unk: !!unk, src: { ...src }, ...(unk && sig ? { sig } : {}) }); if (this.recs.size > 300) this.recs.delete(this.recs.keys().next().value); this.check(site); } catch (e) { dbg(e, 'rec'); } }, stats(site) { const now = Date.now(); let n = 0, u = 0; for (const r of this.recs.values()) { if ((site && r.site !== site) || now - r.t > PD_WINDOW) continue; n++; if (r.unk) u++; } return { n, u, ratio: n ? u / n : 0 }; }, check(site) { const s = this.stats(site); const bad = s.n >= PD_WARN_MIN && s.ratio >= PD_WARN_RATIO; this.flag = bad ? { site, ...s } : null; if (bad && !this.warned) { this.warned = true; pdToast(pdT('toast', { site: site.toUpperCase(), u: s.u, n: s.n })); } else if (!bad && s.ratio < PD_WARN_RATIO / 3) this.warned = false; }, clear() { this.recs.clear(); this.flag = null; this.warned = false; } }; const PD_FIELDS = ['status', 'topic', 'frame', 'viewers', 'tags', 'sch']; const PD_ST = ['offline', 'online', 'away', 'show']; const prEl = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }; const prSvg = d => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`; const prSrcCls = s => (s ? s.split(':')[0] : ''); const PR_CSS = RS_PULT_CSS + ` .bm-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font:13px/1.4 Onest,system-ui,sans-serif} .bm-modal.pr-modal{width:min(760px,96vw);background:var(--pb)} .rs-pult .pr-ok,.rs-pult .pr-warn{align-items:flex-start;line-height:1.4;font-weight:400} .rs-pult .pr-ok svg,.rs-pult .pr-warn svg{margin-top:1px!important;filter:none!important} .rs-pult .pr-ok{background:#12301f;border-color:#2b7a4b;color:#b6f0cf}.rs-pult .pr-ok svg{color:#b6f0cf!important} .rs-pult .pr-warn{background:#3a2410;border-color:#c77d2a;color:#ffd9a8}.rs-pult .pr-warn svg{color:#ffd9a8!important} .pr-btns{display:flex;gap:6px} .pr-btns button{flex:1 1 0;min-width:0;min-height:44px;margin:0;padding:0 12px;border-radius:12px;border:1px solid var(--pl);background:var(--pt);color:#fff;font:500 14px/1.2 Onest,system-ui,sans-serif;display:flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:background .12s,transform .12s} .pr-btns button:hover{background:color-mix(in srgb,var(--pt) 85%,#fff)} .pr-btns button:active{transform:scale(.97)} .pr-btns button:focus-visible,.bm-close:focus-visible{outline:2px solid var(--th);outline-offset:2px} .pr-btns svg{display:block;flex:none} .rs-pult .hd-sec.pr-hs{display:flex!important;justify-content:space-between} .pr-sum{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} .pr-t{min-width:0;padding:8px 12px;border-radius:12px;border:1px solid var(--pl);background:var(--pt)} .pr-t b{display:block;font-size:12px;font-weight:500;opacity:.6} .pr-t div{display:flex;flex-wrap:wrap;gap:2px 8px;margin-top:2px;font-size:13px} .pr-wrap{overflow-x:auto;border-radius:12px;border:1px solid var(--pl);background:var(--pt)} .pr-wrap table{border-collapse:collapse;width:max-content;min-width:100%;font-size:12px} .pr-wrap th,.pr-wrap td{text-align:left;padding:8px 10px;border-bottom:1px solid rgba(255,255,255,.07);white-space:nowrap} .pr-wrap th{font-weight:500;color:#9a9aa6;border-bottom-color:var(--pl)} .pr-wrap tr:last-child td{border-bottom:0} .pr-wrap th:first-child,.pr-wrap td:first-child{position:sticky;left:0;z-index:1;background:var(--pt)} .pr-leg{display:flex;flex-wrap:wrap;gap:4px 12px;padding:4px 4px 0;font-size:11px} .api{color:#6fd39a}.json{color:#7db7ff}.html{color:#b0b0ba}.regex{color:#ffb454}.listing{color:#c79bff} .miss{color:#ffb454}.dim{color:#8a8a96}.bad{color:#ff6b6b;font-weight:600}`; const ParserReport = { host: null, close() { this.host?.remove(); this.host = null; }, exportText() { return JSON.stringify({ version: '4.2.3', at: new Date().toISOString(), warn: { ratio: PD_WARN_RATIO, min: PD_WARN_MIN }, stats: ParserDiag.stats(), records: [...ParserDiag.recs.values()] }, null, 1); }, copy() { const txt = this.exportText(); try { if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(txt).catch(() => this.copyOld(txt)); } catch (e) { dbg(e, 'copy'); } this.copyOld(txt); }, copyOld(txt) { try { const ta = document.createElement('textarea'); ta.value = txt; ta.style.cssText = 'position:fixed;left:-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); } catch (e) { dbg(e, 'copyOld'); } }, open() { this.close(); const host = document.createElement('div'); host.style.cssText = 'all:initial;position:fixed;inset:0;z-index:2147483647'; host.style.setProperty('--th', AppState.theme); const sh = host.attachShadow({ mode: 'open' }); const st = prEl('style'); st.textContent = PR_CSS; const ov = prEl('div', 'bm-overlay'); ov.onclick = e => { if (e.target === ov) this.close(); }; const modal = prEl('div', 'bm-modal rs-pult pr-modal'); const hd = prEl('div', 'bm-hdr'); const title = prEl('span', 'bm-title'); title.innerHTML = `<span class="rs-ic">${icons.list}</span>`; title.append(prEl('span', '', pdT('title'))); const bX = prEl('button', 'bm-close'); bX.type = 'button'; bX.innerHTML = ICON_CLOSE; bX.onclick = () => this.close(); hd.append(title, bX); const bd = prEl('div', 'bm-list'); const recs = [...ParserDiag.recs.values()].sort((a, b) => (b.unk - a.unk) || (b.t - a.t)); bd.append(this._banner(recs), this._buttons(), this._summary(recs), this._models(recs)); modal.append(bd, hd); ov.append(modal); sh.append(st, ov); (document.body || document.documentElement).appendChild(host); this.host = host; const onKey = e => { if (e.key === 'Escape') { this.close(); document.removeEventListener('keydown', onKey, true); } }; document.addEventListener('keydown', onKey, true); }, _banner(recs) { const stats = ParserDiag.stats(); const banner = (cls, icon, text) => { const b = prEl('div', `rs-sum ${cls}`); b.innerHTML = icon; b.append(prEl('span', '', text)); return b; }; const IC_OK = prSvg('<circle cx="12" cy="12" r="9"/><path d="M8.5 12.5l2.5 2.5 4.5-5"/>'); const IC_WARN = prSvg('<path d="M12 4l9 16H3z"/><path d="M12 10v4M12 17.5v.01"/>'); if (!recs.length) return banner('pr-ok', IC_OK, pdT('nodata')); if (ParserDiag.flag) return banner('pr-warn', IC_WARN, pdT('warn', { u: stats.u, n: stats.n, p: Math.round(PD_WARN_RATIO * 100) })); return banner('pr-ok', IC_OK, pdT('ok', { u: stats.u, n: stats.n })); }, _buttons() { const btns = prEl('div', 'pr-btns'); const bCopy = prEl('button'); bCopy.type = 'button'; bCopy.innerHTML = prSvg('<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/>'); const bCopyT = prEl('span', '', pdT('copy')); bCopy.append(bCopyT); bCopy.onclick = async () => { await this.copy(); bCopyT.textContent = pdT('copied'); setTimeout(() => { bCopyT.textContent = pdT('copy'); }, 1500); }; const bClr = prEl('button'); bClr.type = 'button'; bClr.innerHTML = prSvg('<path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/>'); bClr.append(prEl('span', '', pdT('reset'))); bClr.onclick = () => { ParserDiag.clear(); this.open(); }; btns.append(bCopy, bClr); return btns; }, _srcTile(f, recs) { const cnt = new Map(); for (const r of recs) { const s = f === 'status' && r.unk ? '?' : (r.src[f] || ''); if (!s && (r.st === 0 && f !== 'status')) continue; cnt.set(s || '—', (cnt.get(s || '—') || 0) + 1); } const line = prEl('div'); [...cnt.entries()].sort((a, b) => b[1] - a[1]).forEach(([s, n]) => { line.append(prEl('span', s === '?' ? 'bad' : (s === '—' ? 'dim' : prSrcCls(s)), `${s} ${n}`)); }); if (!cnt.size) line.append(prEl('span', 'dim', '—')); const tile = prEl('div', 'pr-t'); tile.append(prEl('b', '', f), line); return tile; }, _summary(recs) { const gSum = prEl('div', 'rs-g'); gSum.append(prEl('div', 'hd-sec', pdT('sumT'))); const sum = prEl('div', 'pr-sum'); for (const f of PD_FIELDS) sum.append(this._srcTile(f, recs)); gSum.append(sum); return gSum; }, _modelRow(r) { const tr = prEl('tr'); tr.append(prEl('td', '', r.slug), prEl('td', 'dim', r.site)); PD_FIELDS.forEach(f => { const s = r.src[f] || ''; let td; if (f === 'status') td = r.unk ? prEl('td', 'bad', `? ${pdT('unknown')}`) : prEl('td', prSrcCls(s), `${PD_ST[r.st] || r.st} · ${s || '—'}`); else if (s) td = prEl('td', prSrcCls(s), s); else td = prEl('td', r.st > 0 && !r.unk && (f === 'topic' || f === 'frame' || f === 'viewers') ? 'miss' : 'dim', '—'); tr.append(td); }); const age = Math.round((Date.now() - r.t) / 1000); tr.append(prEl('td', 'dim', age < 90 ? `${age}s` : `${Math.round(age / 60)}m`)); if (r.unk && r.sig) tr.title = JSON.stringify(r.sig); return tr; }, _models(recs) { const gMod = prEl('div', 'rs-g'); const hs = prEl('div', 'hd-sec pr-hs'); hs.append(prEl('span', '', pdT('models')), prEl('span', '', String(recs.length))); gMod.append(hs); const wr = prEl('div', 'pr-wrap'), tb = prEl('table'), thr = prEl('tr'); ['model', 'site', ...PD_FIELDS, pdT('age')].forEach(c => thr.append(prEl('th', '', c))); const thead = prEl('thead'); thead.append(thr); tb.append(thead); const tbody = prEl('tbody'); for (const r of recs.slice(0, 150)) tbody.append(this._modelRow(r)); tb.append(tbody); wr.append(tb); gMod.append(wr); const leg = prEl('div', 'pr-leg'); [['api', 'API'], ['json', 'JSON'], ['html', 'HTML'], ['regex', 'regex'], ['listing', 'listing']].forEach(([c, n]) => leg.append(prEl('span', c, `● ${n}`))); gMod.append(leg, prEl('div', 'rs-note', pdT('dash'))); return gMod; } }; const HTTP_RE = /^https?:\/\//i; const FRAME_ALT_RE = /"(?:frameUrl|previewFrame|previewUrl|livePreviewUrl)"\s*:\s*"((?:[^"\\]|\\.)*)"/i; const bcdExtras = bcd => ({ ...(Array.isArray(bcd?.ptags) && bcd.ptags.length ? { ptags: bcd.ptags.slice(0, 30) } : {}), ...(Array.isArray(bcd?.sch) && bcd.sch.length === 7 ? { sch: bcd.sch } : {}) }); const parseProfile = (h, site = 'bc', slug = '') => { let bcd; [h, bcd] = splitBcd(h, site); const roots = site === 'bc' ? bcRoots(h) : []; const R = resolveStatusEx(h, site, bcd, roots, slug); const status = R.st, lk = R.lk; const src = { status: R.src }; const done = info => { Object.defineProperty(info, 'unk', { value: !!R.unk, enumerable: false, writable: true, configurable: true }); const sig = R.unk ? { len: isStr(h) ? h.length : 0, head: String(h || '').slice(0, 100).replace(/\s+/g, ' '), roots: roots.map(r => Object.keys(r).slice(0, 8).join(',')) } : null; ParserDiag.rec(site, slug, src, R.unk, info.status, sig); return info; }; if (siteProfile[site]) { const info = { status, lk, ...siteProfile[site](h, status, slug) }; src.topic = info.topic ? 'regex' : ''; src.frame = info.frame ? 'regex' : ''; src.viewers = info.viewers != null ? 'regex' : ''; return done(info); } const [topic, tSrc] = status ? bcTopic(h, bcd, roots) : ['', '']; const [frame, fSrc] = bcFrame(h, bcd, roots); const [vn, vSrc] = bcViewersEx(h, roots); const ex = site === 'bc' ? bcExtraSrc(h, roots) : { out: {}, src: { tags: '', sch: '' } }; const extra = site === 'bc' ? { ...ex.out, ...bcdExtras(bcd) } : {}; src.topic = tSrc; src.frame = fSrc; src.viewers = vn != null ? vSrc : ''; src.tags = extra.ptags ? (bcd?.ptags?.length ? 'api:amf' : ex.src.tags) : ''; src.sch = extra.sch ? (bcd?.sch?.length === 7 ? 'api:amf' : ex.src.sch) : ''; return done({ status, lk, topic, frame, ...(vn != null ? { viewers: vn } : {}), ...extra }); }; const withBackoff = async (fn, { tries = 2, base = 500, signal } = {}) => { for (let i = 0; ; i++) { const res = await fn(); if (res !== null || i >= tries - 1 || signal?.aborted) return res; await new Promise(r => setTimeout(r, base * 2 ** i * (0.75 + Math.random() * 0.5))); } }; const MOBILE_UA = 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36'; const badStatus = c => c >= 500 || c === 403 || c === 429; const requestOnce = (url, extra, signal, timeout) => { let headers = {}; if (/\/api\//.test(url)) { headers = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*' }; try { headers.Referer = `${new URL(url).origin}/`; } catch (e) { dbg(e, 'requestOnce'); } } if (extra) headers = { ...headers, ...extra }; return gmReq({ url, headers, signal, ...(timeout && { timeout }) }).then(r => (r.fail || badStatus(r.status) ? null : r.body)); }; const requestPostOnce = (url, data, extra, signal, timeout) => { const headers = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', ...(extra || {}) }; return gmReq({ method: 'POST', url, data, headers, signal, credentials: 'include', ...(timeout && { timeout }) }).then(r => (r.fail || badStatus(r.status) ? null : r.body)); }; const requestHtml = async (url, signal, timeout) => (await withBackoff(() => requestOnce(url, undefined, signal, timeout), { signal })) ?? ''; const STATUS_KEY = /room_status|"status"\s*:|"isLive"|"isOnline"/i; const profileUrls = b => { const slug = modelSlug(b.url), site = urlSite(b.url); if (!slug) return [b.url]; try { if (site === 'cb') return [`https://chaturbate.com/api/chatvideocontext/${encodeURIComponent(slug)}/`, b.url]; if (site === 'sc') { const o = new URL(b.url).origin, e = encodeURIComponent(slug); return [...new Set([`${o}/api/front/v2/models/username/${e}/cam`, `https://stripchat.com/api/front/v2/models/username/${e}/cam`])].concat(b.url); } } catch (e) { dbg(e, 'profileUrls'); } return [b.url]; }; const requestProfileHtml = async (b, signal) => { for (const u of profileUrls(b)) { const h = await requestHtml(u, signal, 4000); if (h && (u === b.url || STATUS_KEY.test(h))) return h; } return ''; }; const BCD_MARK = '\n/*BCD*/'; const bcChatInfo = j => { const r = j?.status === 'success' ? j.result : null, s = r?.chatShowStatusOptions; if (!isObj(s) || s.isAvailable === false || typeof s.isOffline !== 'boolean') return null; return { o: s.isOffline, p: !!(s.isPrivateChat || s.isFullPrivateChat || s.isVoyeurChat), g: !!s.isGroupChat, v: !!s.isVipShow, t: isStr(r.chatTopicOptions?.currentTopic) ? r.chatTopicOptions.currentTopic : '' }; }; const bcChatData = async (b, signal) => { const o = originOf(b.url), slug = modelSlug(b.url); if (!o || !slug) return null; const url = `${o}/get-member-chat-data?username=${encodeURIComponent(slug)}&withMiniProfile=1&liveTab=female`; const hdr = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Referer': `${o}/${encodeURIComponent(slug)}` }; try { return bcChatInfo(JSON.parse((await requestOnce(url, hdr, signal, 4000)) || '')); } catch { return null; } }; const bcCsrfToken = () => { return document.documentElement.getAttribute('data-csrf_value') || document.querySelector('input[name="_csrf_token"]')?.value || document.querySelector('meta[name="csrf-token"]')?.content || ((document.documentElement.innerHTML || '').match(/(?:_csrf_token|csrfToken)["'\s:=]+([a-f0-9]{24,64})/i) || [])[1]; }; const bcRoomInfo = p => { const tags = Array.isArray(p.modelTags) ? p.modelTags.map(x => isStr(x) ? x : (x?.name || x?.title || x?.tag || '')).filter(isStr) : []; const schedule = isWeekSchedule(p.schedule) ? scheduleDays(p.schedule) : null; return { ...(typeof p.isAway === 'boolean' ? { a: p.isAway } : {}), ...(typeof p.isOnline === 'boolean' ? { n: p.isOnline } : {}), ...(tags.length ? { ptags: tags.slice(0, 30) } : {}), ...(isStr(p.frame) ? { f: p.frame } : {}), ...(schedule && schedule.some(Boolean) ? { sch: schedule } : {}) }; }; const parseRoomData = raw => { const j = JSON.parse(raw || ''), p = j?.status === 'success' ? j.performerData : null; return isObj(p) ? bcRoomInfo(p) : null; }; const bcRoomData = async (b, signal) => { const o = originOf(b.url), slug = modelSlug(b.url); if (!o || !slug || location.origin !== o || !document.documentElement) return null; const token = bcCsrfToken(); if (!token) return null; const body = `method=getRoomData&args%5B%5D=${encodeURIComponent(slug)}&args%5B%5D=&args%5B%5D=&_csrf_token=${encodeURIComponent(token)}`; try { const raw = await requestPostOnce(`${o}/tools/amf.php?m=1&t=${Date.now()}`, body, { Referer: `${o}/` }, signal, 4000); return parseRoomData(raw); } catch { return null; } }; const htmlSaysAway = html => /"isAway"\s*:\s*true/i.test(html) || /(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)away/i.test(html); const bcdStatus = (d, html = '') => { if (d.o) return [0, '']; if (d.p) return [3, 'st_private']; if (d.g) return [3, 'st_group']; if (d.v) return [3, 'st_vip']; if (d.a === true || htmlSaysAway(html)) return [2, 'st_away']; return [1, '']; }; const requestProfile = async (b, signal) => { if (urlSite(b.url) !== 'bc') return requestProfileHtml(b, signal); const [h, d, room] = await Promise.all([requestProfileHtml(b, signal), bcChatData(b, signal), bcRoomData(b, signal)]); const payload = { ...(d || {}), ...(room || {}) }; return (d || room) ? (h || '') + BCD_MARK + JSON.stringify(payload) : h; }; const BC_LIST_TTL = 20000, BC_LIST_PAGE = 500, BC_LIST_MAX = 4000; const BC_EXTRA_TABS = ['male', 'couples', 'trans']; const bcListCache = new Map(); const originOf = u => { try { return new URL(u).origin; } catch { return ''; } }; const bcListCandidates = j => { const c = Array.isArray(j?.models) ? [...j.models] : []; const nav = j?.nav; if (!nav || typeof nav !== 'object') return c; if (nav.current && typeof nav.current === 'object') c.push(nav.current); for (const key of ['next', 'prev']) if (Array.isArray(nav[key])) c.push(...nav[key]); return c; }; const bcListEat = (map, txt) => { try { const j = JSON.parse(txt); for (const m of bcListCandidates(j)) { if (m && isStr(m.username) && m.viewers != null && Number.isFinite(+m.viewers)) { map.set(m.username.toLowerCase(), +m.viewers); if (isStr(m.room) && m.room) BC_ROOM.set(m.username.toLowerCase(), { room: m.room, t: Date.now() }); } } return +(j?.total_count ?? j?.nav?.total_count) || 0; } catch { return 0; } }; const bcListTab = async (origin, tab, signal) => { const ck = `${origin}|${tab}`, hit = bcListCache.get(ck); if (hit && Date.now() - hit.t < BC_LIST_TTL) return hit.map; const base = `${origin}/tools/listing_v3.php?livetab=${encodeURIComponent(tab)}&is_mobile=true&limit=${BC_LIST_PAGE}`; const hdr = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Referer': `${origin}/` }; const map = new Map(); const eat = txt => bcListEat(map, txt); const total = eat((await requestOnce(base, hdr, signal, 12000)) || ''); const offs = []; for (let o = BC_LIST_PAGE; o < Math.min(total, BC_LIST_MAX); o += BC_LIST_PAGE) offs.push(o); const rest = await Promise.all(offs.map(o => requestOnce(`${base}&offset=${o}`, hdr, signal, 12000))); rest.forEach(t => t && eat(t)); if (map.size) bcListCache.set(ck, { t: Date.now(), map }); return map; }; const applyProfile = (b, { frame, ...info }) => { const same = (x, y) => (Array.isArray(x) || Array.isArray(y)) ? JSON.stringify(x) === JSON.stringify(y) : x === y; const changed = Object.keys(info).some(k => !same(b[k], info[k])) || !!(frame && b.frame !== frame); if (info.viewers != null) b.vts = Date.now(); Object.assign(b, info); if (frame) b.frame = frame; return changed; }; const AVATAR_RES = [ /(?:property=["']og:image["']|name=["']og:image["'])[^>]*?content=["']([^"']+)["']/i, /content=["']([^"']+)["'][^>]*?property=["']og:image["']/i, /"(?:profileImage|avatarUrl)"\s*:\s*"([^"]+)"/i ]; const extractAvatar = html => { for (const re of AVATAR_RES) { const m = html.match(re); if (m?.[1]) return toHttps(m[1].replace(/\\/g, '')); } return ''; }; const ogImage = () => toHttps(document.querySelector('meta[property="og:image"]')?.content); const bcAvatar = name => { const sd = Helpers.getModelSecretData(); const own = (sd?.chatHost?.username || '').toLowerCase() === name.toLowerCase(); return toHttps(own ? sd?.chatHeaderOptions?.profileImage : '') || toHttps(document.querySelector('.chat-info-avatar img, .chat-header-avatar img, .profile-photo img')?.src); }; const scAvatar = () => toHttps(document.querySelector('img.image-background--round[src*="avatars"]')?.src) || ogImage(); const cbAvatar = name => name ? `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(name.toLowerCase())}.jpg` : ogImage(); const AVATAR_BY_SITE = new Map([['bc', bcAvatar], ['cb', cbAvatar]]); const pageAvatar = name => (AVATAR_BY_SITE.get(SITE) || scAvatar)(name); const GM_IMG = new Map(); const CB_HQ_FRAME = true; const cbRiUrl = slug => `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(String(slug).toLowerCase())}.jpg`; const cbFrameUrl = slug => CB_HQ_FRAME ? `https://jpeg.live.mmcdn.com/stream?room=${encodeURIComponent(String(slug).toLowerCase())}` : cbRiUrl(slug); const frameBust = u => { try { const o = new URL(u); o.searchParams.set(/^jpeg\.live\./.test(o.hostname) ? 'f' : 't', /^jpeg\.live\./.test(o.hostname) ? String(Math.random()) : String(Date.now())); return o.href; } catch { return `${String(u).split('?')[0]}?t=${Date.now()}`; } }; const cbHqToRi = u => { const m = /^https?:\/\/jpeg\.live\.mmcdn\.com\/stream\?(?:[^#]*&)?room=([^&#]+)/i.exec(String(u)); return m ? cbRiUrl(decodeURIComponent(m[1])) : ''; }; const IMG_DEBUG = Store.get('cbhp_debug', false) === true; const imgReferer = url => { let h = ''; try { h = new URL(url).hostname; } catch { return ''; } if (/bongocams/.test(h)) return 'https://bongocams.com/'; if (/bongacams|bgicdn|bcvcdn|webcamsluts/.test(h)) return 'https://bongacams.com/'; if (/stripchat|doppiocdn/.test(h)) return 'https://stripchat.com/'; if (/chaturbate|mmcdn/.test(h)) return 'https://chaturbate.com/'; return ''; }; const blobToData = blob => new Promise(resolve => { const fr = new FileReader(); fr.onload = () => resolve(typeof fr.result === 'string' ? fr.result : ''); fr.onerror = () => resolve(''); fr.readAsDataURL(blob); }); const gmImgFetch = async url => { const ref = imgReferer(url); const hdr = { Accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8' }; const tries = ref && ref !== `${location.origin}/` ? [{ ...hdr, Referer: ref }, hdr] : [hdr]; for (const headers of tries) { const r = await gmReq({ url, type: 'blob', timeout: 12000, headers }); if (r.status >= 200 && r.status < 300 && r.body && r.body.size) { const d = await blobToData(r.body); if (d) return d; } if (IMG_DEBUG) console.warn('[cams-unchained] img fail', r.status, r.fail, !!headers.Referer, url); if (r.status === 404 || r.status === 410) break; } return ''; }; const gmImgDataRaw = (url, fresh) => { if (!url || /^data:/.test(url) || typeof GM_xmlhttpRequest !== 'function') return Promise.resolve(''); if (!fresh && GM_IMG.has(url)) return GM_IMG.get(url); const p = gmImgFetch(url); if (fresh) return p; GM_IMG.set(url, p); if (GM_IMG.size > 400) GM_IMG.delete(GM_IMG.keys().next().value); p.then(d => { if (!d && GM_IMG.get(url) === p) GM_IMG.delete(url); }); return p; }; const gmImgData = (url, fresh) => gmImgDataRaw(url, fresh).then(d => { if (d) return d; const ri = cbHqToRi(url); return ri ? gmImgDataRaw(frameBust(ri), true) : d; }); const tryGm = (img, u, fail) => { if (!u || /^data:/.test(u) || img._gm === u || typeof GM_xmlhttpRequest !== 'function') return false; img._gm = u; gmImgData(u).then(d => { if (d && img.isConnected !== false && (!img.classList.contains('bm-prev') || img.hasAttribute('data-frame'))) img.src = d; else if (fail) fail(); }); return true; }; const frameFail = img => { const b = img._bRef; const giveUp = () => { img._gen = (img._gen || 0) + 1; img._isUpdating = false; img.removeAttribute('data-frame'); img.style.display = 'none'; }; if (!b || !img.isConnected) return giveUp(); if (img._rec && Date.now() - img._rec < 60000) return giveUp(); img._rec = Date.now(); requestProfile(b).then(h => { const info = h ? parseProfile(h, urlSite(b.url), modelSlug(b.url)) : null; const fr = info && info.status ? info.frame : ''; if (!fr || !img.isConnected) return giveUp(); b.frame = fr; const next = frameBust(fr); const tmp = new Image(); tmp.onload = () => { if (!img.isConnected) return; img.setAttribute('data-frame', fr); img._u = next; b._lastImg = next; img._gm = null; img.src = next; img.style.display = 'block'; }; tmp.onerror = () => gmImgData(next, true).then(d => { if (d && img.isConnected) { img.setAttribute('data-frame', fr); img._u = next; b._lastImg = next; img.src = d; img.style.display = 'block'; } else giveUp(); }); tmp.src = next; }).catch(giveUp); }; const paintViewers = (stats, txt) => { const key = txt + '|' + AppState.lang; if (stats._last === key) return; const n = parseCount(txt); const shown = Number.isFinite(n) ? fmtCount(n) : txt; const sp = stats.querySelector('span'); if (sp && stats.querySelector('svg')) sp.textContent = shown; else stats.innerHTML = `${icons.eye}<span>${escHTML(shown)}</span>`; stats.title = Number.isFinite(n) ? n.toLocaleString() : ''; stats._last = key; }; const needsExtra = (x, now) => isObj(x) && isStr(x.url) && isSafeUrl(x.url) && urlSite(x.url) === 'bc' && (!x._pf || (!x.sch && now - x._pf > 600000)); const frameThrottled = (img, b) => { if (!b || b.status !== 2) return false; img._skip = (img._skip || 0) + 1; return img._skip % 5 !== 0; }; const paintStatusDot = (stat, st) => { const col = STATUS_COLORS[st] || '#888'; stat.style.background = col; stat.style.boxShadow = st ? `0 0 8px ${col}` : '0 0 4px rgba(0,0,0,0.5)'; }; const paintViewsBadge = (viewEl, b) => { const vw = bmViews(b), vwTxt = vw ? fmtCount(vw) : ''; if (viewEl._t === vwTxt) return; viewEl._t = vwTxt; viewEl.innerHTML = vwTxt ? `${icons.eye}${vwTxt}` : ''; }; const refreshFrame = img => { const url = img?.getAttribute('data-frame'); if (!url || img._isUpdating) return; const b = img._bRef; if (frameThrottled(img, b)) return; img._isUpdating = true; const gen = img._gen = (img._gen || 0) + 1; const next = frameBust(url); const alive = () => img.isConnected && img._gen === gen && img.getAttribute('data-frame') === url; const done = () => { if (img._gen === gen) img._isUpdating = false; }; const show = src => { img._u = next; if (b) b._lastImg = next; img._k = (b && b._lastData ? 'd' : '') + next; img.src = src; img.style.display = 'block'; }; const viaGm = () => gmImgData(next, true).then(d => { if (d && alive()) { if (b) b._lastData = d; if (d !== img._lastD) { img._lastD = d; show(d); } } else if (alive() && !(img.complete && img.naturalWidth > 0)) frameFail(img); done(); }); if (img._gmOnly) return viaGm(); const tmp = new Image(); const guard = setTimeout(() => { tmp.onload = tmp.onerror = null; done(); }, 20000); tmp.onload = () => { clearTimeout(guard); if (alive()) { if (b) b._lastData = ''; img._lastD = ''; show(next); } done(); }; tmp.onerror = () => { clearTimeout(guard); img._gmOnly = true; viaGm(); }; tmp.src = next; }; const initialsAva = name => { const s = String(name || '?'), t = (s.replace(/[^\p{L}\p{N}]/gu, '').slice(0, 2) || '?').toUpperCase(); let h = 0; for (const ch of s) h = (h * 31 + ch.charCodeAt(0)) >>> 0; const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' fill='hsl(${h % 360},50%,42%)'/><text x='32' y='41' font-size='26' font-weight='700' text-anchor='middle' fill='#fff' font-family='sans-serif'>${t}</text></svg>`; return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }; const bindAvatar = (img, name, getList, onFail) => { let i = 0, list = null; const next = () => { list ||= [...new Set(getList().filter(Boolean))]; while (i < list.length) { const u = list[i++]; if (u) { img._cur = u; img.src = u; return; } } img.onerror = null; img._fb = 1; img.src = initialsAva(name); onFail?.(); }; img._fb = 0; img.onerror = () => { if (img._fb === 0 && tryGm(img, img._cur, next)) return; next(); }; next(); }; const STATUS_COLORS = { 0: '#e74c3c', 1: '#2ecc71', 2: '#f39c12', 3: '#9b59b6' }; const bmLive = b => +((b.status ?? 0) > 0); const bmGroups = () => [...new Set(AppState.bookmarks.map(b => b.g).filter(Boolean))].sort((a, b) => a.localeCompare(b)); const bmLabel = (b, l) => b.name + (b.lk && l[b.lk] ? ` (${l[b.lk]})` : ''); const bmSeen = (b, st) => st === 0 && b.seen ? fmtAgo(b.seen) : ''; const bmViews = b => (b.status > 0 && b.vts && Date.now() - b.vts < 7200000) ? (b.viewers || 0) : 0; const WMARKS = [[0, 'eyeNo', 'w_new'], [1, 'eyeOk', 'w_seen']]; const bmWatch = (b, hist) => (((hist || histLoad()).m[bmKey(b.url)]?.s || 0) >= 30 ? 1 : 0); const BM_CMP = { date: (x, y) => x.i - y.i, name: (x, y) => x.b.name.localeCompare(y.b.name), online: (x, y) => (bmLive(y.b) - bmLive(x.b)) || (x.i - y.i), last: (x, y) => (bmLive(y.b) - bmLive(x.b)) || ((y.b.seen || 0) - (x.b.seen || 0)) || (x.i - y.i), viewers: (x, y) => (bmLive(y.b) - bmLive(x.b)) || (bmViews(y.b) - bmViews(x.b)) || (x.i - y.i) }; const ST_FILTERS = [ ['hidePrivate', 'bc_bm_hide_priv', 'st_private', 'bm_priv'], ['hideGroup', 'bc_bm_hide_group', 'st_group', 'st_group'], ['hideVip', 'bc_bm_hide_vip', 'st_vip', 'st_vip'], ['hideAway', 'bc_bm_hide_away', 'st_away', 'st_away'] ]; const bmStatusKey = b => { const st = b.status ?? 0; return b.lk || (st === 3 ? 'st_private' : st === 2 ? 'st_away' : ''); }; class BookmarkManager { constructor(uiController) { this.ui = uiController; this.overlay = null; this.list = null; if (!Store.get('bc_mig_stf1', false)) { if (Store.get('bc_bm_hide_priv', false)) { Store.set('bc_bm_hide_group', true); Store.set('bc_bm_hide_vip', true); } Store.set('bc_mig_stf1', true); } ST_FILTERS.forEach(([prop, key]) => { this[prop] = Store.get(key, false); }); this.hideOffline = Store.get('bc_bm_hide_off', false); { const wf = Store.get('bc_bm_wf', -1); this.wFilter = [0, 1].includes(wf) ? wf : null; } this._els = new WeakMap(); this._lang = AppState.lang; this.build(); this._xTimer = setInterval(() => { this.bgExtra(); }, 8000); this._statusTimer = setInterval(() => { if (!realHidden() && this.isOpen()) this.checkStatuses(); }, 20000); onPageEvent('visibilitychange', () => { if (!realHidden() && this.isOpen()) this.checkStatuses(); }); } applyRemote(json) { try { const arr = JSON.parse(json); if (!Array.isArray(arr)) return; const editing = this.ui.shadow.activeElement?.matches?.('textarea'); if (!mergeRemoteBookmarks(arr, editing ? ['note'] : [])) return; if (this.isOpen() && !editing) this.render(); } catch (e) { dbg(e, 'BookmarkManager.applyRemote'); } } async checkStatuses() { if (!AppState.bookmarks?.length) return; if (!this._abortCtrl || this._abortCtrl.signal.aborted) this._abortCtrl = new AbortController(); const { signal } = this._abortCtrl; const runs = this._runs || (this._runs = new Set()); const busy = this._busy || (this._busy = new Set()); const sortType = Store.get('bc_bm_sort_type', 'date'); const now = Date.now(); const queue = AppState.bookmarks.filter(b => isSafeUrl(b.url) && !busy.has(b) && (!b._chk || now - b._chk > 15000)) .sort((x, y) => (y.status > 0) - (x.status > 0) || (x._chk || 0) - (y._chk || 0)); if (queue.length) { let changedAny = false; const mark = () => { changedAny = true; }; this._vp = this.prefetchViewers(queue, signal); const worker = async () => { while (queue.length && !signal.aborted) { while (bcPtrDown && !signal.aborted) await new Promise(r => setTimeout(r, 150)); if (signal.aborted) break; const b = queue.shift(); if (!b) break; busy.add(b); try { await this.refreshOne(b, signal, sortType, mark); } catch (e) { dbg(e, 'checkStatuses.worker'); } busy.delete(b); } }; const run = Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker)).then(() => { if (changedAny) saveBookmarks(); }); runs.add(run); run.finally(() => runs.delete(run)); } await Promise.all([...runs]); } async loadExtra(b, signal) { const pu = bcProfileUrl(b); if (!pu) return {}; for (const hd of [{ 'User-Agent': MOBILE_UA }, null]) { const h = await requestOnce(pu, hd, signal); if (signal?.aborted) break; const ex = h ? parseBcExtra(h) : {}; if (ex.sch || ex.ptags) return ex; } return {}; } async bgExtra(force) { if (this._xBusy || (!force && (realHidden() || bcPtrDown))) return 'busy'; const now = Date.now(); const b = AppState.bookmarks.find(x => needsExtra(x, now)); if (!b) return 'none'; this._xBusy = true; b._pf = now; let changed = false; try { const ex = await this.loadExtra(b); if (applyProfile(b, ex)) { changed = true; saveBookmarks(); if (this.isOpen() && this.mode === 'fav') this.render(); } } catch (e) { dbg(e, 'BookmarkManager.bgExtra'); } this._xBusy = false; return changed; } async prefetchViewers(queue, signal) { this._vmap = this._vmap || new Map(); this._vx = this._vx || new Map(); const origins = new Set(queue.filter(b => urlSite(b.url) === 'bc').map(b => originOf(b.url)).filter(Boolean)); origins.forEach(o => this._vx.delete(o)); await Promise.all([...origins].map(async o => { try { this._vmap.set(o, new Map(await bcListTab(o, 'female', signal))); } catch (e) { dbg(e, 'BookmarkManager.prefetchViewers'); } })); } async viewersFor(b, signal) { await this._vp; const o = originOf(b.url), slug = String(modelSlug(b.url) || '').toLowerCase(); const maps = this._vmap; if (!maps || !o || !slug) return null; const own = () => maps.get(o)?.get(slug); if (own() != null) return own(); if (!this._vx.has(o)) { this._vx.set(o, Promise.all(BC_EXTRA_TABS.map(t => bcListTab(o, t, signal).catch(() => new Map()))).then(ms => { const base = maps.get(o) || new Map(); ms.forEach(m => m.forEach((v, k) => { if (!base.has(k)) base.set(k, v); })); maps.set(o, base); }).catch(() => {})); } await this._vx.get(o); return own() ?? null; } async refreshOne(b, signal, sortType, mark) { const h = await requestProfile(b, signal); if (signal.aborted || !h) return; const prevChk = b._chk, prevMob = b._mob; b._chk = Date.now(); const info = await this.enrichInfo(b, h, signal); if (signal.aborted) { b._chk = prevChk; b._mob = prevMob; return; } if (info.status > 0 && !info.unk) this.touchSeen(b, mark); if (!applyProfile(b, info)) return; mark(); this.onStatusChange(b, sortType); } async enrichInfo(b, h, signal) { const info = parseProfile(h, urlSite(b.url), modelSlug(b.url)); if (info.unk) { const keep = Number.isFinite(+b.status) && (!b._kt || Date.now() - b._kt < PD_STALE_MS); if (keep) { info.status = +b.status; delete info.lk; } delete info.topic; delete info.frame; } else b._kt = Date.now(); if (urlSite(b.url) === 'bc' && info.status > 0) { const vn = await this.viewersFor(b, signal); if (vn != null) info.viewers = vn; } if (this.needsExtra(b, info)) { b._mob = Date.now(); Object.assign(info, await this.loadExtra(b, signal)); } if (info.status === 0 && b._live && Date.now() - b._live < 90000) info.status = 1; return info; } needsExtra(b, info) { return urlSite(b.url) === 'bc' && !info.sch && (!b._mob || Date.now() - b._mob > 600000); } touchSeen(b, mark) { b.seen = Date.now(); if (b._sv && b.seen - b._sv <= 300000) return; b._sv = b.seen; mark(); } isOpen() { return !!this.overlay?.classList.contains('open'); } onStatusChange(b, sortType) { if (!this.isOpen()) return; b._updateItemDOM?.(); if (RESORT_SORTS.has(sortType) || this.hideOffline || ST_FILTERS.some(([prop]) => this[prop])) { clearTimeout(this._resortTimer); const tick = () => { if (!this.overlay.classList.contains('open') || this.mode !== 'fav') return; if (this.ui.shadow.activeElement?.matches?.('textarea,select')) { this._resortTimer = setTimeout(tick, 1000); return; } this.render(); }; this._resortTimer = setTimeout(tick, 600); } } build() { this.buildOverlay(); const modal = mk('div', 'bm-modal fav-modal'); this.modal = modal; this.mode = 'fav'; const hdr = mk('div', 'bm-hdr'); this.buildTabs(); const closeBtn = mk('button', 'bm-close'); closeBtn.innerHTML = '×'; closeBtn.onclick = () => this.hide(); const { btnTogglePrev, btnSpeed } = this.buildPreviewControls(); const btnFavNav = this.buildFavNavSwitch(); const hideSw = this.buildHideSwitches(modal); const sortWrap = this.buildSortControls(); const btnCols = this.buildColsButton(); const btnAv = this.buildAvatarRefresh(); const setPanel = this.buildSettingsPanel({ btnFavNav, btnSpeed, btnAv, ...hideSw }); const btnGear = mk('button', 'bm-hbtn bm-gear'); btnGear.innerHTML = icons.gear; bindGear(btnGear, setPanel); const tools = mk('div', 'bm-tools'); this.wsegHost = mk('div', 'bm-wseg-host'); tools.append(this.wsegHost, btnTogglePrev, btnCols, btnGear); hdr.append(this.title, closeBtn); const tbar = mk('div', 'bm-tbar'); tbar.append(tools, setPanel); this.buildSearch(sortWrap); this.tagsWrap = mk('div', 'bm-tags-wrap'); this.list = mk('div', 'bm-list'); this.applyCols?.(); this.bindHostEvents(); const foot = mk('div', 'bm-foot'); this.btnAdd = mk('button', 'bm-add'); this.btnAdd.onclick = () => this.add(); foot.append(this.btnAdd); modal.append(hdr, tbar, this.searchWrap, this.tagsWrap, this.list, foot); this.overlay.appendChild(modal); this.ui.shadow.appendChild(this.overlay); } buildOverlay() { this.overlay = mk('div', 'bm-overlay fav-ov'); this.overlay.onclick = e => { if (e.target === this.overlay && Date.now() - (this._opAt || 0) > 500) this.hide(); }; this.overlay.addEventListener('click', e => { if (e.timeStamp - (this._opPerf || 0) < 400) { e.preventDefault(); e.stopPropagation(); } }, true); this.overlay.addEventListener('touchmove', e => { if (this.onSlider(e) || this.hasScrollableAncestor(e)) return; if (e.cancelable) e.preventDefault(); }, { passive: false }); } isScrollable(n) { const cs = getComputedStyle(n); return (/(auto|scroll)/.test(cs.overflowY) && n.scrollHeight > n.clientHeight + 1) || (/(auto|scroll)/.test(cs.overflowX) && n.scrollWidth > n.clientWidth + 1); } onSlider(e) { return e.composedPath().some(n => n instanceof Element && n.matches('input[type=range],.mv-sk')); } hasScrollableAncestor(e) { for (const n of e.composedPath()) { if (n === this.overlay) return false; if (n instanceof Element && this.isScrollable(n)) return true; } return false; } buildTabs() { this.title = mk('span', 'bm-title'); this.tabFav = mk('button', 'bm-tbtn on'); this.tabHist = mk('button', 'bm-tbtn'); this.tabFav.onclick = () => this.setMode('fav'); this.tabHist.onclick = () => this.setMode('hist'); const tabs = mk('div', 'bm-seg bm-tabs'); this.tabCal = mk('button', 'bm-tbtn'); this.tabCal.onclick = () => this.setMode('cal'); this.tabMv = mk('button', 'bm-tbtn'); this.tabMv.onclick = () => this.setMode('mv'); tabs.append(this.tabFav, this.tabMv, this.tabHist, this.tabCal); this.title.append(tabs); } buildPreviewControls() { const btnTogglePrev = mk('button', 'bm-hbtn bm-prevbtn'); const btnSpeed = mk('div', 'bm-spdseg'); const spdBtns = PREV_SPD_SEG.map(v => { const b = mk('button', 'bm-spdopt'); b.type = 'button'; b.textContent = prevSpdLabel(v); b.onclick = () => { Store.set('bc_prev_spd', v); this.updatePrevUI(); this.stopPreviews(); this.render(); }; btnSpeed.append(b); return [v, b]; }); if (!Store.get('bc_mig_prev1', false)) { Store.set('bc_previews', true); Store.set('bc_mig_prev1', true); } this.prevEn = Store.get('bc_previews', true) !== false; this.updatePrevUI = () => { btnTogglePrev.classList.toggle('on', !!this.prevEn); btnTogglePrev.innerHTML = this.prevEn ? icons.eye : icons.eyeOff; btnTogglePrev.title = L().st_prev; btnSpeed.style.opacity = this.prevEn ? '' : '.4'; btnSpeed.style.pointerEvents = this.prevEn ? '' : 'none'; btnSpeed.title = prevSpdTitle(); spdBtns.forEach(([v, b]) => { b.classList.toggle('on', prevSpd() === v); b.disabled = !this.prevEn; }); }; this.updatePrevUI(); btnTogglePrev.onclick = () => { this.prevEn = !this.prevEn; Store.set('bc_previews', this.prevEn); this.updatePrevUI(); this.ui.multiView?.syncPrev(); this.render(); }; return { btnTogglePrev, btnSpeed }; } buildFavNavSwitch() { const btnFavNav = mk('button', 'bm-sw'); this.updateFavNavUI = () => { btnFavNav.classList.toggle('on', FavNav.on); }; this.updateFavNavUI(); btnFavNav.onclick = () => { FavNav.set(!FavNav.on); this.updateFavNavUI(); }; return btnFavNav; } buildHideSwitches(modal) { const btnHideTopic = mk('button', 'bm-sw'); const btnHideGrp = mk('button', 'bm-sw'); const btnHidePT = mk('button', 'bm-sw'); const btnHideNote = mk('button', 'bm-sw'); const applyHide = () => { const t = Store.get('bc_bm_hide_topic', true) !== false; const g = Store.get('bc_bm_hide_grp', true) !== false; btnHideTopic.classList.toggle('on', t); btnHideGrp.classList.toggle('on', g); modal.classList.toggle('bm-no-topic', t); modal.classList.toggle('bm-no-grp', g); const pt = Store.get('bc_bm_hide_ptags', true) !== false; btnHidePT.classList.toggle('on', pt); modal.classList.toggle('bm-no-ptags', pt); const hn = Store.get('bc_bm_hide_note', true) !== false; btnHideNote.classList.toggle('on', hn); modal.classList.toggle('bm-no-note', hn); }; applyHide(); btnHideTopic.onclick = () => { Store.set('bc_bm_hide_topic', !(Store.get('bc_bm_hide_topic', true) !== false)); applyHide(); }; btnHideGrp.onclick = () => { Store.set('bc_bm_hide_grp', !(Store.get('bc_bm_hide_grp', true) !== false)); applyHide(); }; btnHideNote.onclick = () => { Store.set('bc_bm_hide_note', !(Store.get('bc_bm_hide_note', true) !== false)); applyHide(); }; btnHidePT.onclick = () => { Store.set('bc_bm_hide_ptags', !(Store.get('bc_bm_hide_ptags', true) !== false)); applyHide(); this.render(); }; return { btnHideTopic, btnHideGrp, btnHidePT, btnHideNote }; } buildSortControls() { const sortWrap = mk('div', 'bm-sort-wrap'); this.btnSortType = mk('button', 'bm-hbtn'); this.btnSortDir = mk('button', 'bm-hbtn'); this.updateSortUI = () => { const type = Store.get('bc_bm_sort_type', 'date'); const isRev = Store.get('bc_bm_sort_rev', false); const l = L(); const map = { date: [icons.clock, l.sort_date], name: [icons.name, l.sort_name], online: [icons.dot, l.sort_live], last: [icons.eye, l.sort_last], viewers: [icons.eye, l.sort_viewers] }; const [icon, label] = map[type] || map.date; this.btnSortType.innerHTML = icon + label; this.btnSortDir.innerHTML = isRev ? icons.arrowUp : icons.arrowDown; }; this.btnSortType.onclick = () => { const types = ['date', 'name', 'online', 'last', 'viewers']; const cur = Store.get('bc_bm_sort_type', 'date'); Store.set('bc_bm_sort_type', types[(types.indexOf(cur) + 1) % types.length]); this.updateSortUI(); this.render(); }; this.btnSortDir.onclick = () => { Store.set('bc_bm_sort_rev', !Store.get('bc_bm_sort_rev', false)); this.updateSortUI(); this.render(); }; this.updateSortUI(); sortWrap.append(this.btnSortType, this.btnSortDir); return sortWrap; } buildColsButton() { const btnCols = mk('button', 'bm-hbtn bm-colsbtn'); const colsIcons = COLS_ICONS; const colsKey = COLS_KEY; this.applyCols = () => { const n = Store.get(colsKey, 1) === 2 ? 2 : 1; this.list.style.setProperty('grid-template-columns', `repeat(${n},minmax(0,1fr))`, 'important'); this.list.classList.toggle('bm-c1', n === 1); btnCols.innerHTML = colsIcons[n - 1]; btnCols.title = L().st_cols; }; btnCols.onclick = () => { Store.set(colsKey, Store.get(colsKey, 1) === 2 ? 1 : 2); this.applyCols(); }; return btnCols; } buildAvatarRefresh() { const btnAv = mk('button', 'bm-hbtn bm-spd bm-avbtn'); btnAv.textContent = '↻'; let avBusy = false; btnAv.onclick = async () => { if (avBusy) return; avBusy = true; btnAv.disabled = true; const queue = AppState.bookmarks.slice(), total = queue.length; let done = 0; const upd = () => { btnAv.textContent = `${done}/${total}`; }; upd(); const worker = async () => { while (queue.length) { const b = queue.shift(); try { await this.fetchAvatar(b); } catch (e) { dbg(e, 'buildAvatarRefresh.worker'); } done++; upd(); } }; await Promise.all([worker(), worker(), worker()]); btnAv.textContent = '✓'; setTimeout(() => { btnAv.textContent = '↻'; btnAv.disabled = false; avBusy = false; }, 1500); }; return btnAv; } buildSettingsPanel({ btnFavNav, btnHideTopic, btnHideGrp, btnHidePT, btnHideNote, btnSpeed, btnAv }) { const row = mkSetRow; const setPanel = mk('div', 'bm-setp'); setPanel.append( row(L().st_favnav, btnFavNav), row(L().st_hide_topic, btnHideTopic), row(L().st_hide_grp, btnHideGrp), row(L().st_ptags || i18n.EN.st_ptags, btnHidePT), row(L().st_hide_note || i18n.EN.st_hide_note, btnHideNote), (() => { const r = row(L().st_prev_spd, btnSpeed); r.classList.add('bm-setrow-spd'); return r; })(), (() => { const r = row(L().st_av_refresh, btnAv); r.classList.add('bm-setrow-av'); return r; })() ); return setPanel; } buildSearch(sortWrap) { this.query = ''; this.searchWrap = mk('div', 'bm-search-wrap'); const sBox = mk('div', 'bm-search-box'); const sIco = mk('span', 'bm-search-ico'); sIco.innerHTML = icons.search; this.searchIn = mk('input', 'bm-search-in'); this.searchIn.type = 'text'; this.searchIn.enterKeyHint = 'search'; this.searchIn.autocomplete = 'off'; this.searchIn.spellcheck = false; this.searchIn.setAttribute('autocapitalize', 'off'); this.searchClr = mk('button', 'bm-search-clr'); this.searchClr.textContent = '×'; let sTimer = 0; const onQuery = () => { const v = this.searchIn.value; this.query = v.trim().toLowerCase(); this.searchClr.style.display = v ? 'flex' : 'none'; clearTimeout(sTimer); sTimer = setTimeout(() => this.render(), 120); }; this.searchIn.oninput = onQuery; this.searchIn.onkeydown = e => { e.stopPropagation(); if (e.key === 'Escape' && this.searchIn.value) { e.preventDefault(); this.searchIn.value = ''; onQuery(); } }; this.searchClr.onclick = () => { this.searchIn.value = ''; onQuery(); this.searchIn.focus(); }; sBox.append(sIco, this.searchIn, this.searchClr); this.searchWrap.append(sBox, sortWrap); } bindHostEvents() { window.addEventListener('bc_topic', e => { try { const t = String(JSON.parse(e.detail).t || '').trim(), nm = String(currentSite.getName() || '').toLowerCase(); if (!nm || !t) return; AppState.bookmarks.forEach(b => { if (String(b.name).toLowerCase() !== nm || urlSite(b.url) !== siteKey(location.hostname) || !(b.status > 0)) return; if (applyProfile(b, { topic: t })) b._updateItemDOM?.(); }); } catch (e) { dbg(e, 'BookmarkManager.bindHostEvents'); } }); window.addEventListener('bc_viewers', e => { try { const d = JSON.parse(e.detail), nm = String(currentSite.getName() || '').toLowerCase(); if (!nm || !(d.n >= 0)) return; AppState.bookmarks.forEach(b => { if (String(b.name).toLowerCase() !== nm || urlSite(b.url) !== siteKey(location.hostname)) return; if (applyProfile(b, { viewers: d.n })) b._updateItemDOM?.(); }); } catch (e) { dbg(e, 'BookmarkManager.bindHostEvents'); } }); window.addEventListener('bc_hls', () => { if (this.isOpen()) this.list.querySelectorAll('.bm-item').forEach(i => i._syncHls?.()); }); this._observer = new IntersectionObserver(entries => entries.forEach(e => e.target._inView = e.isIntersecting), { root: this.list, threshold: 0.05 }); } hide() { this.overlay.classList.remove('open'); this.ui.multiView?.suspend(); clearTimeout(this._resortTimer); this.stopPreviews?.(); try { this._abortCtrl?.abort(); } catch (e) { dbg(e, 'BookmarkManager.hide'); } } enterMode(mode, fromOpen) { if (mode === 'fav') { if (!fromOpen) { this.render(); this.checkStatuses(); } return; } this.stopPreviews?.(); const manager = { hist: 'historyManager', cal: 'calendarManager', mv: 'multiView' }[mode]; this.ui[manager]?.show(); } setMode(m, fromOpen) { const mode = this.mode = ['hist', 'cal', 'mv'].includes(m) ? m : 'fav'; for (const k of ['hist', 'cal', 'mv']) this.modal.classList.toggle(`mode-${k}`, mode === k); const tabs = { fav: this.tabFav, hist: this.tabHist, cal: this.tabCal, mv: this.tabMv }; for (const k of Object.keys(tabs)) tabs[k].classList.toggle('on', mode === k); if (mode !== 'mv') this.ui.multiView?.suspend(); this.enterMode(mode, fromOpen); } updateLabels() { this.tabFav.innerHTML = `${icons.star}<span class="bm-tl">${escHTML(L().bm_title)}</span>`; this.tabHist.innerHTML = `${icons.clock}<span class="bm-tl">${escHTML(L().hist_title)}</span>`; this.tabCal.innerHTML = `${icons.cal}<span class="bm-tl">${escHTML(L().cal_title)}</span>`; this.tabMv.innerHTML = `${icons.grid}<span class="bm-tl">${escHTML(mvT('mv_title'))}</span>`; this.tabMv.title = mvT('mv_title'); if (this.ui.calendarManager?.isOpen()) this.ui.calendarManager.render(); this.searchIn.placeholder = L().bm_find; this.updateSortUI?.(); this.updatePrevUI?.(); this.applyCols?.(); } open(mode) { syncBookmarks(); this.updateLabels(); this.query = ''; this.searchIn.value = ''; this.searchClr.style.display = 'none'; this.setMode(mode, true); if (this.mode === 'fav') { this.render(); this.list.scrollTop = 0; this.checkStatuses(); } this._opAt = Date.now(); this._opPerf = performance.now(); this.overlay.classList.add('open'); } statusSeg(l) { const toggle = (prop, key) => () => { this[prop] = !this[prop]; Store.set(key, this[prop]); this.render(); }; const seg = mk('div', 'bm-seg'); seg.append( bmTbtn(l.bm_online, this.hideOffline, toggle('hideOffline', 'bc_bm_hide_off'), l.bm_online_t), ...ST_FILTERS.map(([prop, key, lk, label]) => bmTbtn(l[label], this[prop], toggle(prop, key), l[lk])) ); return seg; } watchSeg(l) { const setW = v => () => { this.wFilter = this.wFilter === v ? null : v; Store.set('bc_bm_wf', this.wFilter == null ? -1 : this.wFilter); this.render(); }; const wseg = mk('div', 'bm-seg bm-wseg'); wseg.append( ...WMARKS.map(([v, ic, key]) => bmTbtn(icons[ic], this.wFilter === v, setW(v), l[key])) ); return wseg; } appendGroupChips(chips, l) { const groups = bmGroups(); if (this.currentGroup && !groups.includes(this.currentGroup)) this.currentGroup = null; if (!groups.length) return; const setGrp = g => () => { this.currentGroup = this.currentGroup === g ? null : g; this.render(); }; chips.append( bmTbtn(`${icons.folder}${escHTML(l.grp_all)}`, !this.currentGroup, () => { this.currentGroup = null; this.render(); }), ...groups.map(g => bmTbtn(`${icons.folder}${escHTML(g)}`, this.currentGroup === g, setGrp(g))) ); } topPtags(hidePT, ptc) { if (hidePT) return []; return [...ptc.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 30).map(e => e[0]); } appendTagChips(chips, l) { const allTags = new Set(AppState.bookmarks.flatMap(b => noteHashTags(b).map(t => t.toLowerCase()))); const ptc = new Map(); AppState.bookmarks.forEach(b => (b.ptags || []).forEach(t => countInto(ptc, t))); const hidePT = Store.get('bc_bm_hide_ptags', true) !== false; const ptop = this.topPtags(hidePT, ptc); if (this.currentTagFilter && !allTags.has(this.currentTagFilter) && (hidePT || !ptc.has(this.currentTagFilter))) this.currentTagFilter = null; if (!allTags.size && !ptop.length) { this.currentTagFilter = null; return; } const setTag = tag => () => { this.currentTagFilter = this.currentTagFilter === tag ? null : tag; this.render(); }; const pim = ptIcoMap(); chips.append( bmTbtn(l.bm_all, !this.currentTagFilter, () => { this.currentTagFilter = null; this.render(); }), ...[...allTags].sort().map(tag => bmTbtn(tag, this.currentTagFilter === tag, setTag(tag))), ...(allTags.size && ptop.length ? [mk('span', 'bm-chips-sep')] : []), ...ptop.map(tag => bmTbtn(`${ptImg(pim.get(tag), 'bm-ptic') || icons.tag}${escHTML(tag)}`, this.currentTagFilter === tag, setTag(tag))) ); } renderTags() { if (!this.tagsWrap) return; const l = L(); const chips = mk('div', 'bm-chips'); this.tagsWrap.replaceChildren(this.statusSeg(l)); this.wsegHost && this.wsegHost.replaceChildren(this.watchSeg(l)); this.tagsWrap.style.display = 'flex'; this.appendGroupChips(chips, l); this.appendTagChips(chips, l); if (chips.children.length) { this.tagsWrap.append(chips); const sx = this._chipsX || 0; chips.scrollLeft = sx; requestAnimationFrame(() => { if (chips.isConnected && chips.scrollLeft !== sx) chips.scrollLeft = sx; }); chips.addEventListener('scroll', () => { this._chipsX = chips.scrollLeft; }, { passive: true }); } } itemTopRow(item, b) { const top = mk('div', 'bm-item-top'); const link = mk('a', 'bm-link'); link.href = isSafeUrl(b.url) ? b.url : '#'; const { img, applyAva } = this.itemAvatar(b); const stat = mk('span', 'bm-stat'); const nameWrap = mk('div', 'bm-nwrap'); const nameEl = mk('span', 'bm-name'); nameEl.textContent = b.name; const seenEl = mk('span', 'bm-seen'); const viewEl = mk('span', 'bm-view'); nameWrap.append(stat, nameEl, viewEl, seenEl); link.append(img, nameWrap); this.bindItemFrameHold(item, b); img.draggable = false; img.style.webkitTouchCallout = 'none'; this.bindNameCopyHold(nameWrap, nameEl, b); const { pinBtn, delBtn } = this.itemPinDel(b); top.append(link, pinBtn, delBtn); return { top, link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, delBtn }; } itemStatusFn(item, b, p) { const { link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, updateTopicDOM, applyPT, applyPreview } = p; const applyW = () => { item.dataset.w = String(bmWatch(b, this._hist || (this._hist = histLoad()))); }; return () => { const l = L(), st = b.status ?? 0; link.href = isSafeUrl(b.url) ? linkFor(b.url) : '#'; applyAva(); img.title = l.fr_hold; item.classList.toggle('pinned', !!b.pin); pinBtn.classList.toggle('on', !!b.pin); pinBtn.title = b.pin ? l.bm_unpin : l.bm_pin; item.dataset.st = String(st); paintStatusDot(stat, st); nameEl.textContent = bmLabel(b, l); seenEl.textContent = bmSeen(b, st); seenEl.title = l.seen_tip; paintViewsBadge(viewEl, b); viewEl.title = l.viewers_tip; updateTopicDOM(b.topic || ''); applyW(); applyPT(); applyPreview(st); }; } renderItem(b) { const cached = this._els.get(b); if (cached) { cached.fillGrp(); cached.apply(); cached.item._syncNote?.(); cached.item._syncHls?.(); b._updateItemDOM = cached.apply; return cached.item; } const item = mk('div', 'bm-item'); const { top, link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, delBtn } = this.itemTopRow(item, b); const { actionsRow, syncHls } = this.itemActions(b); item._syncHls = syncHls; item._syncHls(); const { noteInput, autoResize, syncNote } = this.itemNote(b); item._syncNote = syncNote; const { topicEl, updateTopicDOM } = this.itemTopic(b); const { grpSel, fillGrp } = this.itemGroup(b); const { ptagsEl, applyPT } = this.itemPtags(b); const { previewImg, applyPreview } = this.itemPreview(b); item.onclick = e => { if (isSafeUrl(b.url) && !e.target.closest('button, textarea, select, a.tp-btn')) location.href = linkFor(b.url); }; item.append(top, actionsRow, ptagsEl, topicEl, grpSel, noteInput, previewImg); this.itemMore(item, b, actionsRow, pinBtn, delBtn); const applyStatus = this.itemStatusFn(item, b, { link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, updateTopicDOM, applyPT, applyPreview }); applyStatus(); b._updateItemDOM = applyStatus; this._observer?.observe(item); this._els.set(b, { item, fillGrp, apply: applyStatus, resize: () => requestAnimationFrame(autoResize) }); return item; } itemAvatar(b) { const img = mk('img', 'bm-avatar'); const applyAva = () => { const k = b.avatar || ''; if (img._k === k) return; img._k = k; bindAvatar(img, b.name, () => [ toHttps(b.avatar), toHttps((this._hist || (this._hist = histLoad())).m[bmKey(b.url)]?.a) ], () => { if (!b._avFix) { b._avFix = 1; this.fetchAvatar(b); } }); }; applyAva(); return { img, applyAva }; } bindItemFrameHold(item, b) { Helpers.bindHold(item, { ok: e => !e.target.closest('button, textarea, select, input, a.tp-btn, .bm-nwrap'), onHold: () => openFrameView(this.ui, b) }); } bindNameCopyHold(nameWrap, nameEl, b) { nameWrap.style.userSelect = nameWrap.style.webkitUserSelect = nameWrap.style.webkitTouchCallout = 'none'; const flash = ok => { nameEl.textContent = ok ? '✓' : 'ERR'; setTimeout(() => { nameEl.textContent = b.name; }, 800); }; Helpers.bindHold(nameWrap, { always: true, onHold: () => Helpers.copy(b.name, () => flash(true), () => flash(false)) }); } itemPinDel(b) { const delBtn = mk('button', 'bm-del'); delBtn.innerHTML = icons.del; delBtn.title = L().bm_del; delBtn.onclick = e => { e.preventDefault(); e.stopPropagation(); dropBookmark(b); this.render(); }; const pinBtn = mk('button', 'bm-pin'); pinBtn.innerHTML = icons.star; pinBtn.onclick = e => { e.preventDefault(); e.stopPropagation(); if (b.pin) delete b.pin; else b.pin = true; saveBookmarks(); this.render(); }; return { pinBtn, delBtn }; } itemActions(b) { const makeAct = (html, fn, onHold) => { const btn = mk('button', 'bm-act-btn'); btn.innerHTML = html; if (onHold) Helpers.bindLongPress(btn, () => fn(btn), onHold); else btn.onclick = e => { e.preventDefault(); e.stopPropagation(); fn(btn); }; return btn; }; const onPage = () => String(b.name).toLowerCase() === String(currentSite.getName()).toLowerCase() && urlSite(b.url) === siteKey(location.hostname); const canHls = () => onPage() || !!MV_RES[urlSite(b.url)]; const getHls = () => { const hit = BM_HLS.get(b.url); if (hit && Date.now() - hit.at < 45000) return hit.p; const p = Promise.resolve().then(async () => { if (onPage()) { const cur = getStreamUrl(); if (cur) return cur; } const r = await mvResolve({ t: 'bm', name: String(b.name), url: b.url }); return (r && r.url) || ''; }).catch(() => ''); BM_HLS.set(b.url, { at: Date.now(), p }); if (BM_HLS.size > 40) { const now = Date.now(); for (const [k, v] of BM_HLS) if (now - v.at > 45000) BM_HLS.delete(k); } p.then(u => { if (!u && BM_HLS.get(b.url)?.p === p) BM_HLS.delete(b.url); }); return p; }; const busy = (btn, on) => { btn.style.opacity = on ? '.55' : ''; }; const actionsRow = mk('div', 'bm-actions-row bm-acts'); actionsRow.append( makeAct(`${icons.search} <span>${L().search}</span>`, () => { noFade(this.overlay, this.ui.searchManager?.overlay); this.overlay.classList.remove('open'); this.ui.searchManager?.open(b.name, true); }), makeAct(`${icons.hls} <span>HLS</span>`, async btn => { busy(btn, true); const u = await getHls(); busy(btn, false); copyHls(u, ok => Helpers.flashBtn(btn, ok)); }, async () => { const btn = actionsRow.children[1]; busy(btn, true); const u = await getHls(); busy(btn, false); shareHls(u, `${b.name} Stream`, () => Helpers.flashBtn(btn, false)); } ), makeAct(`${icons.copy} <span>${L().copy}</span>`, btn => { Helpers.copy(linkFor(b.url), () => Helpers.flashBtn(btn, true), () => Helpers.flashBtn(btn, false)); }) ); const hlsBtn = actionsRow.children[1]; hlsBtn.addEventListener('pointerdown', () => { if (canHls()) getHls(); }); const syncHls = () => { hlsBtn.style.opacity = canHls() ? '' : '.4'; hlsBtn.title = 'HLS'; }; return { actionsRow, syncHls }; } itemMore(item, b, actionsRow, pinBtn, delBtn) { const more = mk('button', 'bm-act-btn bm-more'); more.type = 'button'; more.innerHTML = icons.dots; const pop = mk('div', 'bm-mpop'); let off = null; const close = () => { pop.classList.remove('open'); more.classList.remove('on'); if (off) { off(); off = null; } if (this._mpopClose === close) this._mpopClose = null; }; const defs = [ ['bm-mrow-link', () => icons.copy, () => { const t = String(L().copy); return t.charAt(0).toLocaleUpperCase() + t.slice(1).toLocaleLowerCase(); }, () => Helpers.copy(linkFor(b.url), () => Helpers.flashBtn(more, true), () => Helpers.flashBtn(more, false))], ['bm-mrow-pin', () => icons.star, () => (b.pin ? L().bm_unpin : String(L().bm_pin).split(' ')[0]), () => pinBtn.click()], ['bm-mrow-del', () => icons.del, () => L().bm_del, () => delBtn.click()] ]; const rows = defs.map(([cls, ic, label, fn]) => { const r = mk('button', 'bm-mrow ' + cls); r.type = 'button'; r.onclick = e => { e.preventDefault(); e.stopPropagation(); close(); fn(); }; pop.append(r); return () => { r.innerHTML = ic() + '<span></span>'; r.lastChild.textContent = label(); }; }); more.onclick = e => { e.preventDefault(); e.stopPropagation(); if (pop.classList.contains('open')) return close(); this._mpopClose?.(); rows.forEach(f => f()); pop.classList.add('open'); more.classList.add('on'); const onDown = ev => { const path = ev.composedPath(); if (!path.includes(pop) && !path.includes(more)) close(); }; document.addEventListener('pointerdown', onDown, true); off = () => document.removeEventListener('pointerdown', onDown, true); this._mpopClose = close; }; actionsRow.append(more); item.append(pop); } itemNote(b) { const noteInput = mk('textarea', 'bm-note'); noteInput.rows = '1'; noteInput.style.cssText = 'resize:none;overflow:hidden;min-height:36px;white-space:pre-wrap;word-break:break-word;'; noteInput.placeholder = L().note_ph; noteInput.value = b.note || ''; const autoResize = () => { noteInput.style.height = 'auto'; noteInput.style.height = `${Math.max(36, noteInput.scrollHeight)}px`; }; const syncNote = () => { const v = b.note || ''; if (document.activeElement !== noteInput && noteInput.value !== v) { noteInput.value = v; requestAnimationFrame(autoResize); } }; requestAnimationFrame(autoResize); let noteT = 0; noteInput.oninput = e => { requestAnimationFrame(autoResize); b.note = e.target.value.trim(); clearTimeout(noteT); bmDirty = true; noteT = setTimeout(saveBookmarks, 400); }; noteInput.onblur = () => { if (noteT) { clearTimeout(noteT); noteT = 0; saveBookmarks(); } }; noteInput.onchange = () => { clearTimeout(noteT); noteT = 0; saveBookmarks(); this.ui.flash(this.btnAdd, 'OK'); this.render(); }; noteInput.onkeydown = e => e.stopPropagation(); return { noteInput, autoResize, syncNote }; } itemGroup(b) { const grpSel = mk('button', 'bm-grp bm-grp-btn'); grpSel.type = 'button'; const groupList = () => { const gs = bmGroups(); if (b.g && !gs.includes(b.g)) gs.push(b.g); return gs; }; const fillGrp = () => { const label = b.g || L().grp_none; if (grpSel._lbl === label) return; grpSel._lbl = label; grpSel.innerHTML = `<span>${icons.folder}${escHTML(label)}</span><span class="bm-grp-arr">▾</span>`; }; fillGrp(); grpSel.onclick = e => { e.stopPropagation(); openGroupDialog(this.ui, { current: b.g || '', groups: groupList(), onPick: v => { b.g = v; saveBookmarks(); this.render(); } }); }; grpSel.onkeydown = e => e.stopPropagation(); return { grpSel, fillGrp }; } itemTopic(b) { const topicEl = mk('div', 'bm-topic'); const updateTopicDOM = txt => { const key = `${txt}|${!!b._topicExp}|${AppState.lang}`; if (topicEl._k === key) return; topicEl._k = key; if (!txt) { topicEl.style.display = 'none'; return; } topicEl.style.display = 'block'; if (txt.length > 55) { const txtMore = L().tp_more; const txtLess = L().tp_less; topicEl.innerHTML = `<span>${escHTML(b._topicExp ? txt : txt.slice(0, 55) + '...')} </span><a class="tp-btn" style="color:var(--th);cursor:pointer;font-weight:bold;text-decoration:underline;">${escHTML(b._topicExp ? txtLess : txtMore)}</a>`; topicEl.querySelector('.tp-btn')?.addEventListener('click', ev => { ev.preventDefault(); b._topicExp = !b._topicExp; updateTopicDOM(txt); }); } else topicEl.textContent = txt; }; return { topicEl, updateTopicDOM }; } itemPtags(b) { const ptagsEl = mk('div', 'bm-ptags'); const applyPT = () => { const tg = Array.isArray(b.ptags) ? b.ptags : [], cf = this.currentTagFilter || ''; const key = `${tg.join('|')}#${(Array.isArray(b.ptic) ? b.ptic : []).join('|')}#1`; const syncOn = () => ptagsEl.querySelectorAll('.bm-ptag[data-t]').forEach(c => c.classList.toggle('on', c.dataset.t === cf)); if (ptagsEl._k === key) { syncOn(); return; } ptagsEl._k = key; ptagsEl.replaceChildren(); ptagsEl.style.display = tg.length ? 'flex' : 'none'; const max = 3; tg.slice(0, max).forEach((t, ix) => ptagsEl.append(this.ptagChip(b, t, ix, cf))); if (tg.length > max) { const more = mk('span', 'bm-ptag more'); more.textContent = `+${tg.length - max}`; ptagsEl.append(more); } }; return { ptagsEl, applyPT }; } ptagChip(b, t, ix, cf) { const c = mk('button', `bm-ptag${cf === t.toLowerCase() ? ' on' : ''}`); c.type = 'button'; c.dataset.t = t.toLowerCase(); const u = ptIcoUrl(Array.isArray(b.ptic) ? b.ptic[ix] : ''); if (u) { const im = mk('img', 'bm-ptic'); im.alt = ''; im.decoding = 'sync'; im.referrerPolicy = 'no-referrer'; im.onerror = () => im.remove(); im.src = u; c.append(im); } c.append(document.createTextNode(t)); c.onclick = e => { e.preventDefault(); e.stopPropagation(); this.currentTagFilter = this.currentTagFilter === t.toLowerCase() ? null : t.toLowerCase(); this.render(); }; return c; } itemPreview(b) { const previewImg = mk('img', 'bm-prev'); previewImg.onload = () => previewImg.classList.remove('bm-bad'); previewImg.onerror = () => { previewImg.classList.add('bm-bad'); if (!previewImg.hasAttribute('data-frame')) { previewImg.style.display = 'none'; return; } if (/^data:/.test(previewImg.src) || !tryGm(previewImg, previewImg._u, () => frameFail(previewImg))) frameFail(previewImg); }; const showImg = on => { const d = on ? 'block' : 'none'; if (previewImg.style.display !== d) previewImg.style.display = d; }; const applyPreview = st => { if (!(this.prevEn && b.frame && st !== 0)) { previewImg.removeAttribute('data-frame'); previewImg._gen = (previewImg._gen || 0) + 1; previewImg._isUpdating = false; return showImg(false); } previewImg.setAttribute('data-frame', b.frame); showImg(true); previewImg._bRef = b; b._lastImg ||= frameBust(b.frame); const key = (b._lastData ? 'd' : '') + b._lastImg; if (previewImg._k !== key) { previewImg._k = key; previewImg._u = b._lastImg; previewImg.src = b._lastData || b._lastImg; } }; return { previewImg, applyPreview }; } scanPage() { if (!IS_BC) return false; const key = pageKey(); if (!key) return false; const b = AppState.bookmarks.find(x => isObj(x) && isStr(x.url) && bmKey(x.url) === key); if (!b) return false; const info = parseBcExtra(document.documentElement.outerHTML); if (!info.ptags && !info.sch) return false; if (applyProfile(b, info)) { saveBookmarks(); if (this.isOpen()) this.render(); } return true; } markLive(key, known) { const b = known || AppState.bookmarks.find(x => bmKey(x.url) === key); if (!b) return; b._live = Date.now(); b.seen = b._live; if ((b.status ?? 0) !== 0) return; b.status = 1; clearTimeout(this._vwSave); bmDirty = true; this._vwSave = setTimeout(saveBookmarks, 3000); if (this.isOpen()) this.onStatusChange(b, Store.get('bc_bm_sort_type', 'date')); } setViewers(key, n, known) { const b = known || AppState.bookmarks.find(x => bmKey(x.url) === key); if (!b || !(n >= 0)) return; const changed = b.viewers !== n; b.viewers = n; b.vts = Date.now(); if (!changed) return; clearTimeout(this._vwSave); bmDirty = true; this._vwSave = setTimeout(saveBookmarks, 3000); if (this.isOpen()) this.onStatusChange(b, Store.get('bc_bm_sort_type', 'date')); } addEntry({ name, url, avatar, viewers }) { syncBookmarks(); if (!isSafeUrl(url) || isFavKey(bmKey(url))) return false; const bm = { name: clip(name, 100) || modelSlug(url), url: normUrl(url), avatar: toHttps(avatar), note: '' }; if (viewers > 0) { bm.viewers = viewers; bm.vts = Date.now(); } AppState.bookmarks.push(bm); saveBookmarks(); if (this.isOpen()) this.render(); this.checkStatuses(); if (!bm.avatar) this.fetchAvatar(bm); return true; } removeKey(key, after) { syncBookmarks(); const ex = AppState.bookmarks.find(b => isObj(b) && isStr(b.url) && bmKey(b.url) === key); if (!ex) return false; const run = () => { dropBookmark(ex); if (this.isOpen()) this.render(); if (after) after(); }; if (ex.note || ex.g) askConfirm(this.ui, L().star_del_q, run); else run(); return true; } updateAddBtn() { const name = currentSite.getName(), l = L(); const show = !!name && !isFavName(name); this.btnAdd.disabled = !show; if (show) this.btnAdd.textContent = `${l.bm_add} (${name})`; const foot = this.btnAdd.parentNode; if (foot) foot.style.display = show ? '' : 'none'; } passesFilters(b) { const st = b.status ?? 0, lk = bmStatusKey(b); return !(this.hideOffline && st === 0) && !ST_FILTERS.some(([prop,, key]) => this[prop] && key === lk) && this.matchesText(b) && (this.wFilter == null || bmWatch(b, this._hist) === this.wFilter); } matchesText(b) { const tag = this.currentTagFilter, q = this.query, g = this.currentGroup; const note = b.note || '', grp = b.g || ''; return bmTagMatch(b, tag) && (!g || grp === g) && (!q || `${b.name} ${note} ${grp} ${(b.ptags || []).join(' ')}`.toLowerCase().includes(q)); } syncList(els) { const keep = new Set(els); Array.from(this.list.children).forEach(c => { if (!keep.has(c)) c.remove(); }); els.forEach((el, i) => { const cur = this.list.children[i]; if (cur !== el) this.list.insertBefore(el, cur || null); }); } sortedFiltered() { const dir = Store.get('bc_bm_sort_rev', false) ? -1 : 1; const cmp = BM_CMP[Store.get('bc_bm_sort_type', 'date')] || BM_CMP.date; return AppState.bookmarks.map((b, i) => ({ b, i })) .sort((x, y) => (!!y.b.pin - !!x.b.pin) || dir * cmp(x, y)) .filter(({ b }) => this.passesFilters(b)); } render() { this.ui.thumbStars?.schedule(); this._hist = histLoad(); if (this._lang !== AppState.lang) { this._els = new WeakMap(); this._lang = AppState.lang; } const keepScroll = this.list.scrollTop; this.searchWrap.style.display = AppState.bookmarks.length ? 'flex' : 'none'; AppState.bookmarks.forEach(b => { if (b?._updateItemDOM) b._updateItemDOM = null; }); this.updateAddBtn(); if (!AppState.bookmarks.length) { this.stopPreviews(); this.list.innerHTML = `<div class="bm-empty">${L().bm_empty}</div>`; if (this.tagsWrap) { this.tagsWrap.replaceChildren(); this.tagsWrap.style.display = 'none'; } this.wsegHost?.replaceChildren(); return; } this.renderTags(); this.list.classList.add('bm-gallery-mode'); this.applyCols?.(); const sortedList = this.sortedFiltered(); if (!sortedList.length) { this.list.innerHTML = `<div class="bm-empty">${L().bm_nomatch}</div>`; return; } this.syncList(sortedList.map(({ b }) => this.renderItem(b))); this.list.scrollTop = keepScroll; if (!this._previewTimer && this.prevEn && prevSpd()) { this._previewTimer = setInterval(() => this.tickPreviews(), prevSpd()); } } stopPreviews() { clearInterval(this._previewTimer); this._previewTimer = null; } tickPreviews() { if (!this.overlay.classList.contains('open') || !this.prevEn) return this.stopPreviews(); if (realHidden()) return; this.list.querySelectorAll('.bm-item').forEach(item => { if (item._inView) refreshFrame(item.querySelector('img[data-frame]')); }); } add() { syncBookmarks(); const name = currentSite.getName(); if (!name) return; if (isFavName(name)) return this.ui.flash(this.btnAdd, L().bm_dup); const bm = { name, url: normUrl(location.href), avatar: pageAvatar(name), note: '' }; if (!validBm(bm)) return this.ui.flash(this.btnAdd, 'ERR'); const vn = parseCount(readViewers()); if (vn > 0) { bm.viewers = vn; bm.vts = Date.now(); } AppState.bookmarks.push(bm); saveBookmarks(); this.render(); this.checkStatuses(); if (!bm.avatar) this.fetchAvatar(bm); } async fetchAvatar(bm) { const avatar = extractAvatar((await withBackoff(() => requestOnce(bm.url))) || ''); if (!avatar) return; if (avatar === bm.avatar) return; bm.avatar = avatar; saveBookmarks(); if (this.isOpen()) this.render(); } } const pageHasError = (url, res) => { const raw = res.responseText || '', txt = raw.toLowerCase(); const title = txt.match(/<title[^>]*>(.*?)<\/title>/i)?.[1] || ''; return [ () => res.status === 404 || res.status >= 500, () => ['not found', '404', 'error'].some(t => title.includes(t)), () => /no\s*(?:videos?|results?)\s*found|does\s*not\s*exist|\b0\s*results?\b/i.test(txt), () => url.includes('camshowrecordings') && !raw.includes('class="h1modelpage"'), () => url.includes('pornolab.net') && (/не найден[оа]/.test(txt) || !txt.includes('tlink')) ].some(check => check()); }; const SEARCH_ENGINES = [ ['PornoLab', 'https://pornolab.net/forum/tracker.php?nm=%s'], ['BestCam.tv', 'https://bestcam.tv/model/%s'], ['CamShowRecordings', 'https://www.camshowrecordings.com/?s=%s'], ['LiveCamsRip', 'https://www.livecamsrip.com/%s/profile'], ['CamDownload', 'https://camdownload.net/performer/%s'], ['CamShow.cc', 'https://camshow.cc/streamer/%s'], ['SimpCity', 'https://simpcity.cr/search/?q=%s&o=date', true], ['CamsClips', 'https://www.camsclips.com/search/%s/', true] ]; const SRCH_CSS = `.rs-pult .sr-box{display:flex;align-items:center;gap:8px;height:48px;padding:0 4px 0 12px;border-radius:12px;border:1px solid var(--pl);background:var(--pt)} .rs-pult .sr-box:focus-within{border-color:var(--th)} .rs-pult .sr-box>svg{flex:none;width:20px;height:20px;margin:0!important;color:var(--th);filter:brightness(1.4)} .rs-pult .sr-in{flex:1;min-width:0;height:100%;margin:0!important;padding:0!important;border:0!important;outline:0!important;box-shadow:none!important;background:transparent!important;color:#fff!important;font:500 16px/1.2 Onest,system-ui,sans-serif!important} .rs-pult .sr-x{flex:none;display:flex;align-items:center;justify-content:center;width:40px;height:40px;margin:0;padding:0;border:0;border-radius:10px;background:transparent;color:#fff;opacity:.6;cursor:pointer} .rs-pult .sr-x:active{transform:scale(.92)} .rs-pult .sr-x svg{display:block;width:18px;height:18px;margin:0!important} .rs-pult .sr-row{display:flex;align-items:center;gap:10px;width:100%;min-height:48px;margin:0;padding:0 12px;border:1px solid var(--pl);border-radius:12px;background:var(--pt);color:#fff;font:500 14px/1.2 Onest,system-ui,sans-serif;text-align:start;cursor:pointer;transition:background .12s,transform .12s} .rs-pult .sr-row:hover{background:color-mix(in srgb,var(--pt) 85%,#fff)} .rs-pult .sr-row:active{transform:scale(.98)} .rs-pult .sr-row:focus-visible,.rs-pult .sr-x:focus-visible{outline:2px solid var(--th);outline-offset:2px} .rs-pult .sr-dot{flex:none;width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.25)} .rs-pult .sr-nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rs-pult .sr-ar{flex:none;display:flex;opacity:.5} .rs-pult .sr-ar svg{display:block;width:18px;height:18px;margin:0!important} .rs-pult .sr-row[data-st=check]{opacity:.8} .rs-pult .sr-row[data-st=check] .sr-dot{background:#f39c12} .rs-pult .sr-row[data-st=ok] .sr-dot{background:#2ecc71} .rs-pult .sr-row[data-st=skip] .sr-dot{background:#888} .rs-pult .sr-row[data-st=err] .sr-dot{background:#e74c3c} .rs-pult .sr-row[data-st=err] .sr-nm{opacity:.55} .rs-pult .sr-row[data-st=err] .sr-ar{opacity:.3}`; const LP_CSS = `.rs-pult .sr-in::placeholder{color:rgba(255,255,255,.5)} .rs-pult .hd-sec.lp-hs{display:flex!important;justify-content:space-between} .rs-pult .lp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} .rs-pult .lp-btn{display:flex;align-items:center;gap:8px;min-width:0;min-height:46px;margin:0;padding:0 8px;border:1px solid var(--pl);border-radius:12px;background:var(--pt);color:#fff;font:500 13px/1.2 Onest,system-ui,sans-serif;text-align:start;cursor:pointer;transition:background .12s,transform .12s} .rs-pult .lp-btn:hover{background:color-mix(in srgb,var(--pt) 85%,#fff)} .rs-pult .lp-btn:active{transform:scale(.97)} .rs-pult .lp-btn:focus-visible{outline:2px solid var(--th);outline-offset:2px} .rs-pult .lp-code{flex:none;display:flex;align-items:center;justify-content:center;width:36px;height:24px;border-radius:6px;background:rgba(255,255,255,.1);font-size:11px;font-weight:600;letter-spacing:.4px;font-variant-numeric:tabular-nums} .rs-pult .lp-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rs-pult .lp-auto{width:100%;min-height:48px;padding:0 10px;gap:10px;font-size:14px} .rs-pult .lp-auto .lp-code{width:44px;height:26px} .rs-pult .lp-ck{flex:none;display:flex} .rs-pult .lp-ck svg{display:block;width:18px;height:18px;margin:0!important} .rs-pult .lp-btn.sel{background:var(--th);border-color:transparent;color:var(--pon)} .rs-pult .lp-btn.sel .lp-code{background:rgba(0,0,0,.25)} .rs-pult .lp-empty{padding:8px 4px;font-size:13px;opacity:.7} .rs-pult .lp-btn[hidden],.rs-pult .lp-empty[hidden],.rs-pult .rs-g[hidden]{display:none!important} :host([data-rtl]) .rs-pult .lp-btn{text-align:right}`; const LP_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>'; const SRCH_ARROW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7M8 7h9v9"/></svg>'; const SRCH_CLEAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>'; const sentenceCase = t => { t = String(t || ''); return t.charAt(0) + t.slice(1).toLowerCase(); }; class SearchManager { constructor(uiController) { this.ui = uiController; this.overlay = null; this.input = null; this.btns = []; this._live = new Set(); this._sched = []; this._gen = 0; this.build(); } build() { const { overlay, modal, title, closeBtn, body } = buildModalShell(this.ui.shadow, 'bm-modal rs-modal rs-pult'); this.overlay = overlay; this.modal = modal; this.title = title; modal.style.setProperty('background', 'var(--pb)', 'important'); closeBtn.innerHTML = ICON_CLOSE; const css = document.createElement('style'); css.textContent = RS_PULT_CSS + SRCH_CSS; overlay.appendChild(css); overlay.onclick = e => { if (e.target === overlay && Date.now() - (this._opAt || 0) > 500) overlay.classList.remove('open'); }; closeBtn.onclick = () => overlay.classList.remove('open'); this.btnBack = mk('button', 'bm-close bm-back'); this.btnBack.innerHTML = icons.back; this.btnBack.style.display = 'none'; this.btnBack.onclick = () => { noFade(this.overlay, this.ui.bookmarkManager?.overlay); this.overlay.classList.remove('open'); if (this.ui.bookmarkManager) this.ui.bookmarkManager.open(); }; modal.querySelector('.bm-hdr').insertBefore(this.btnBack, closeBtn); const box = mk('div', 'sr-box'); box.innerHTML = icons.search; this.input = mk('input', 'sr-in'); this.input.type = 'text'; this.input.onkeydown = e => e.stopPropagation(); const clear = mk('button', 'sr-x'); clear.type = 'button'; clear.innerHTML = SRCH_CLEAR; clear.onclick = () => { this.input.value = ''; this.input.focus(); this.input.oninput?.(); }; box.append(this.input, clear); const list = mk('div', 'rs-g'); this.buildSearchButtons(list); this.bindSearchInput(); const grp = mk('div', 'rs-g'); grp.append(box); body.replaceChildren(grp, list); } buildSearchButtons(list) { list.append(textEl('hd-sec', L().sr_sites || (/^(RU|UK|BE)$/i.test(AppState.lang) ? 'Сайты' : 'Sites')), ...SEARCH_ENGINES.map(([txt, tmpl, skipCheck]) => { const urlFn = v => tmpl.replace('%s', encodeURIComponent(v)); const btn = mk('button', 'sr-row'); btn.type = 'button'; btn.innerHTML = `<span class="sr-dot"></span><span class="sr-nm">${escHTML(txt)}</span><span class="sr-ar">${SRCH_ARROW}</span>`; btn.dataset.st = 'idle'; btn.onclick = () => { const v = this.input.value.trim(); if (v) window.open(urlFn(v), '_blank', 'noopener'); else this.ui.flash(btn, 'ERR'); }; this.btns.push({ btn, urlFn, skipCheck: !!skipCheck }); return btn; })); } bindSearchInput() { let debounceTimer; this.input.oninput = () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { this._resetRequests(); const v = this.input.value.trim(); this.btns.forEach((item, index) => { if (!v) { item.btn.dataset.st = 'idle'; } else if (item.skipCheck) { item.btn.dataset.st = 'skip'; } else { this._sched.push(setTimeout(() => this.checkPage(item.urlFn(v), item.btn), index * 150)); } }); }, 600); }; } _resetRequests() { this._gen++; if (this._sched) { this._sched.forEach(clearTimeout); this._sched = []; } this._live.forEach(r => { try { r.abort(); } catch (e) { dbg(e, 'SearchManager._resetRequests'); } }); this._live.clear(); } checkPage(url, btn) { if (!btn.isConnected) return; btn.dataset.st = 'check'; const gen = this._gen; const stale = () => gen !== this._gen || !btn.isConnected; const markErr = () => { if (!stale()) btn.dataset.st = 'err'; }; const req = new AbortController(); this._live.add(req); gmReq({ url, timeout: 8000, signal: req.signal }).then(res => { this._live.delete(req); if (res.fail === 'abort') return; if (res.fail) return markErr(); if (stale()) return; if (pageHasError(url, { status: res.status, responseText: res.body })) return markErr(); btn.dataset.st = 'ok'; }); } open(customName, fromFavs = false) { if (this._resetRequests) this._resetRequests(); if (this.btnBack) this.btnBack.style.display = fromFavs ? 'flex' : 'none'; this.title.innerHTML = `<span class="rs-ic">${icons.search}</span><span>${escHTML(sentenceCase(L().search))}</span>`; const name = (typeof customName === 'string') ? customName : (currentSite.getName() || ''); this.input.value = name; this._opAt = Date.now(); this.overlay.classList.add('open'); if (name) { this.btns.forEach((item, index) => { if (item.skipCheck) { item.btn.dataset.st = 'skip'; } else { item.btn.dataset.st = 'idle'; const t = setTimeout(() => this.checkPage(item.urlFn(name), item.btn), 200 + (index * 150)); this._sched.push(t); } }); } } } class PickManager { constructor(uiController, onDone) { this.ui = uiController; this.onDone = onDone; this.active = false; this.cur = null; this._sx = 0; this._sy = 0; this.box = mk('div', 'pick-box'); this.bar = mk('div', 'pick-bar'); this.info = mk('div', 'pick-info'); const btns = mk('div', 'pick-btns'); this.btnUp = mk('button', 'bm-btn'); this.btnOk = mk('button', 'bm-btn'); this.btnNo = mk('button', 'bm-btn'); btns.append(this.btnUp, this.btnOk, this.btnNo); this.bar.append(this.info, btns); this.ui.shadow.append(this.box, this.bar); this.btnUp.onclick = () => { const p = this.cur?.parentElement; if (p && p !== document.body && p !== document.documentElement) this.setTarget(p); }; this.btnOk.onclick = () => this.confirm(); this.btnNo.onclick = () => this.stop(); this._events = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'touchend']; this._handler = e => { if (e.composedPath().includes(this.ui.host)) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (e.type === 'pointerdown') { this._sx = e.clientX; this._sy = e.clientY; } else if (e.type === 'pointerup' && Math.hypot(e.clientX - this._sx, e.clientY - this._sy) < 10) { this.pickAt(e.clientX, e.clientY); } }; this._sync = () => this.placeBox(); } start() { if (this.active) return; this.active = true; this.cur = null; this.box.style.display = 'none'; this.btnUp.textContent = `↑ ${L().pk_up}`; this.btnOk.textContent = L().pk_ok; this.btnNo.textContent = L().pk_cancel; this.info.textContent = L().pk_hint; this.bar.classList.add('open'); this._events.forEach(ev => window.addEventListener(ev, this._handler, { capture: true, passive: false })); window.addEventListener('scroll', this._sync, { capture: true, passive: true }); window.addEventListener('resize', this._sync, { passive: true }); } stop() { if (!this.active) return; this.active = false; this.cur = null; this._events.forEach(ev => window.removeEventListener(ev, this._handler, true)); window.removeEventListener('scroll', this._sync, true); window.removeEventListener('resize', this._sync); this.box.style.display = 'none'; this.bar.classList.remove('open'); this.onDone?.(); } pickAt(x, y) { const el = document.elementFromPoint(x, y); if (!el || el === document.body || el === document.documentElement || el === this.ui.host) return; this.setTarget(el); } placeBox() { if (!this.cur || !this.cur.isConnected) return; const r = this.cur.getBoundingClientRect(); const s = this.box.style; s.display = 'block'; s.left = `${r.left}px`; s.top = `${r.top}px`; s.width = `${r.width}px`; s.height = `${r.height}px`; } setTarget(el) { this.cur = el; this.placeBox(); const sel = buildSelector(el); let n = 0; try { n = document.querySelectorAll(sel).length; } catch (e) { dbg(e, 'PickManager.setTarget'); } this.info.textContent = `${sel} (${n} ${L().pk_found})`; } confirm() { if (!this.cur) return; const sel = buildSelector(this.cur); try { document.querySelector(sel); } catch { return; } addCustom(sel); applyHideCSS(); this.stop(); } } const textEl = (cls, text, tag = 'div') => { const e = mk(tag, cls); e.textContent = text; return e; }; const firstMatch = c => { for (const x of c.sel) { try { const e = document.querySelector(x); if (e) return e; } catch (e) { dbg(e, 'firstMatch'); } } return null; }; const HIDE_GRP_CHAT = /^(chat_|tips$|tipking$|chathdr$|sc_(topic|king|goal)$|cb_tips$)/; const HIDE_GRP_WIN = /^(popups|joinpop|age18|modelchat|sc_cookie|sc_promo|cb_alerts)$/; const HIDE_PULT_CSS = `.rs-pult .hp-top{display:flex;gap:6px} .rs-pult .hp-top .rs-sum{flex:1;min-width:0} .rs-pult .hp-dt{flex:none;display:flex;align-items:center;gap:6px;min-height:44px;margin:0;padding:0 12px;border-radius:14px;border:1px solid var(--pl);background:var(--pt);color:#fff;font:500 13px/1 Onest,system-ui,sans-serif;cursor:pointer;transition:transform .12s} .rs-pult .hp-dt svg{display:block;flex:none} .rs-pult .hp-dt.on{background:var(--th);border-color:transparent;color:var(--pon)} .rs-pult .hp-dt:active,.rs-pult .hp-btn:active,.rs-pult .hp-pr .bm-tbtn:active{transform:scale(.97)} .rs-pult .hp-pr{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))} .rs-pult .hp-hs{display:flex;align-items:center;justify-content:space-between;padding:2px 4px 0;font-size:11px;font-weight:600;letter-spacing:.4px;text-transform:uppercase;color:#fff;opacity:.62} .rs-pult .hp-list{display:flex;flex-direction:column} .rs-pult .hp-row{display:flex;align-items:center;gap:10px;min-height:48px;padding:10px 4px;border-bottom:1px solid var(--pl);cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;touch-action:manipulation} .rs-pult .hp-row:last-child{border-bottom:0} .rs-pult .hp-tx{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .rs-pult .hp-tx b{font-size:14px;font-weight:500;line-height:1.25;color:#fff} .rs-pult .hp-tx small{font-size:12px;line-height:1.3;color:#fff;opacity:.62} .rs-pult .hp-cmp .hp-tx small{display:none} .rs-pult .hp-nf{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;background:rgba(240,170,60,.16);color:#f0aa3c;font-size:11px;font-weight:400;vertical-align:1px} .rs-pult .hp-sw{flex:none;position:relative;width:46px;height:26px;border-radius:13px;background:rgba(255,255,255,.18);transition:background .22s ease} .rs-pult .hp-sw::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.45);transition:left .22s cubic-bezier(.3,1.3,.5,1),transform .15s} .rs-pult .hp-row:active .hp-sw::after{transform:scaleX(1.12)} .rs-pult .hp-row.on .hp-sw{background:var(--th)} .rs-pult .hp-row.on .hp-sw::after{left:23px} .rs-pult .hp-empty{padding:8px 0;text-align:center;font-size:13px;color:#fff;opacity:.55} .rs-pult .hp-rule{cursor:default} .rs-pult .hp-sel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:12px/1.3 ui-monospace,monospace;color:#fff} .rs-pult .hp-del{flex:none;display:flex;align-items:center;justify-content:center;width:36px;height:36px;margin:0;padding:0;border:0;border-radius:10px;background:var(--pb);color:#fff;cursor:pointer} .rs-pult .hp-del svg{width:18px;height:18px} .rs-pult .hp-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:46px;margin:0;padding:0 12px;border:1px solid var(--pl);border-radius:12px;background:var(--pt);color:#fff;font:500 14px/1.2 Onest,system-ui,sans-serif;cursor:pointer;transition:transform .12s} .rs-pult .hp-btn svg{display:block;flex:none;width:20px;height:20px} .rs-pult .hp-btn.pm{background:var(--th);border-color:transparent;color:var(--pon)} .rs-pult .hp-dt:focus-visible,.rs-pult .hp-btn:focus-visible,.rs-pult .hp-del:focus-visible{outline:2px solid var(--th);outline-offset:2px}`; class HideManager { constructor(uiController) { this.ui = uiController; this.build(); this.picker = new PickManager(uiController, () => this.open()); } build() { const { overlay, modal, title, closeBtn, body } = buildModalShell(this.ui.shadow, 'bm-modal rs-modal rs-pult'); modal.style.setProperty('background', 'var(--pb)', 'important'); closeBtn.innerHTML = ICON_CLOSE; const css = document.createElement('style'); css.textContent = RS_PULT_CSS + HIDE_PULT_CSS; overlay.appendChild(css); overlay.onclick = e => { if (e.target === overlay && Date.now() - (this._opAt || 0) > 500) this.close(); }; closeBtn.onclick = () => this.close(); this.overlay = overlay; this.title = title; this.list = body; } updateLabels() { this.render(); } open() { this.render(); this._opAt = Date.now(); this.overlay.classList.add('open'); } close() { this.overlay.classList.remove('open'); } render() { const l = L(); this.title.innerHTML = `<span class="rs-ic">${icons.eyeOff}</span><span>${escHTML(l.hd_title)}</span>`; this.list.replaceChildren(); const frag = document.createDocumentFragment(); this.renderCats(frag, l); this.renderRules(frag, l); this.list.append(frag); } renderCats(frag, l) { const tx = k => l[k] ?? i18n.EN[k] ?? ''; const plat = detectPlatform(); const cats = HIDE_CATS.filter(c => c.site === SITE && (c.plat === 'b' || c.plat === plat)); const rows = []; const grpOf = c => HIDE_GRP_CHAT.test(c.id) ? 'chat' : HIDE_GRP_WIN.test(c.id) ? 'win' : 'site'; const sumTx = mk('span'); const sum = mk('div', 'rs-sum'); sum.innerHTML = icons.eyeOff; sum.appendChild(sumTx); const dt = mk('button', 'hp-dt'); dt.type = 'button'; dt.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h10M4 18h16"/></svg>'; dt.appendChild(textEl('', tx('hd_details'), 'span')); const det = () => Store.get('bc_hide_det', true) !== false; const paintDet = () => { dt.classList.toggle('on', det()); dt.setAttribute('aria-pressed', String(det())); this.list.classList.toggle('hp-cmp', !det()); }; dt.onclick = () => { Store.set('bc_hide_det', !det()); paintDet(); }; const top = mk('div', 'hp-top'); top.append(sum, dt); const counters = []; const refresh = () => { const cur = Store.get('bc_hide_cfg', {}); sumTx.textContent = tx('hd_hidden').replace('{n}', cats.filter(c => catOn(c, cur)).length).replace('{m}', cats.length); counters.forEach(({ list, el }) => { el.textContent = `${list.filter(c => catOn(c, cur)).length} / ${list.length}`; }); }; this._refresh = refresh; const setAll = fn => { const cur = Store.get('bc_hide_cfg', {}); cats.forEach(c => { cur[c.id] = fn(c); }); Store.set('bc_hide_cfg', cur); rows.forEach(({ c, row }) => row.classList.toggle('on', catOn(c, cur))); applyHideCSS(); refresh(); }; const pr = mk('div', 'rs-chips hp-pr'); [[l.hd_p_min, c => HIDE_PRESETS.min.includes(c.id)], [l.hd_p_clean, c => HIDE_PRESETS.clean.includes(c.id)], [l.hd_all, () => true], [l.hd_off, () => false]].forEach(([label, fn]) => { const b = mk('button', 'bm-tbtn'); b.textContent = label; b.onclick = () => setAll(fn); pr.append(b); }); const g0 = mk('div', 'rs-g'); g0.append(textEl('hp-hs', l.hd_cats), pr, textEl('rs-note', l.hd_peek)); frag.append(top, g0); const cfg = Store.get('bc_hide_cfg', {}); [['site', 'hd_g_site'], ['chat', 'hd_g_chat'], ['win', 'hd_g_win']].forEach(([gk, tk]) => { const list = cats.filter(c => grpOf(c) === gk); if (!list.length) return; const cnt = mk('span'); counters.push({ list, el: cnt }); const hs = mk('div', 'hp-hs'); hs.append(textEl('', tx(tk), 'span'), cnt); const box = mk('div', 'hp-list'); list.forEach(c => { const row = this.catRow(c, cfg, l); rows.push({ c, row }); box.append(row); }); const g = mk('div', 'rs-g'); g.append(hs, box); frag.append(g); }); paintDet(); refresh(); } catRow(c, cfg, l) { const found = !!firstMatch(c); const pair = HIDE_L[AppState.lang]?.[c.id] || (AppState.lang === 'RU' ? [c.ru, c.dru] : [c.en, c.den]); const row = mk('div', `hp-row${catOn(c, cfg) ? ' on' : ''}${found ? '' : ' na'}`); const txt = mk('span', 'hp-tx'); const name = mk('b'); name.append(document.createTextNode(pair[0])); if (!found) name.append(textEl('hp-nf', l.hd_na, 'span')); txt.append(name); if (pair[1]) txt.append(textEl('', pair[1], 'small')); row.append(txt, mk('span', 'hp-sw')); this.bindCatRow(row, c); return row; } bindCatRow(row, c) { let timer = null, peeking = false, savedY = 0; const peekOn = () => { const el = firstMatch(c); if (!el) return; peeking = true; savedY = window.scrollY; applyHideCSS(c.id); let st = document.getElementById('bc-unchained-peek'); if (!st) { st = mk('style'); st.id = 'bc-unchained-peek'; (document.head || document.documentElement).appendChild(st); } st.textContent = c.sel.map(x => `${x}{outline:3px solid #ff2d95!important;outline-offset:-3px!important}`).join('\n'); this.overlay.classList.add('peek'); const r = el.getBoundingClientRect(); if (r.bottom < 0 || r.top > window.innerHeight) el.scrollIntoView({ block: 'center' }); }; const peekOff = () => { clearTimeout(timer); if (!peeking) return; document.getElementById('bc-unchained-peek')?.remove(); applyHideCSS(); this.overlay.classList.remove('peek'); if (Math.abs(window.scrollY - savedY) > 1) window.scrollTo(0, savedY); setTimeout(() => { peeking = false; }, 350); }; row.addEventListener('pointerdown', () => { clearTimeout(timer); timer = setTimeout(peekOn, 450); }); ['pointerup', 'pointercancel', 'pointerleave'].forEach(ev => row.addEventListener(ev, peekOff)); row.addEventListener('contextmenu', e => e.preventDefault()); row.onclick = () => { if (peeking) return; const cur = Store.get('bc_hide_cfg', {}); cur[c.id] = !catOn(c, cur); Store.set('bc_hide_cfg', cur); row.classList.toggle('on', cur[c.id]); applyHideCSS(); this._refresh?.(); }; } renderRules(frag, l) { const g = mk('div', 'rs-g'); g.append(textEl('hp-hs', l.hd_rules)); const rules = getCustom(); if (!rules.length) g.append(textEl('hp-empty', l.hd_none)); rules.forEach(sel => { const row = mk('div', 'hp-row hp-rule'); const t = textEl('hp-sel', sel, 'span'); t.title = sel; const del = mk('button', 'hp-del'); del.innerHTML = icons.del; del.onclick = () => { removeCustom(sel); applyHideCSS(); this.render(); }; row.append(t, del); g.append(row); }); const pick = mk('button', 'hp-btn pm'); pick.innerHTML = `${icons.target}<span>${escHTML(l.hd_pick)}</span>`; pick.onclick = () => { this.close(); this.picker.start(); }; g.append(pick); if (rules.length) { const clr = textEl('hp-btn', l.hd_clear, 'button'); clr.onclick = () => { setCustom([]); applyHideCSS(); this.render(); }; g.append(clr); } const reset = textEl('hp-btn', l.hd_reset, 'button'); reset.onclick = () => { const cfgNow = Store.get('bc_hide_cfg', {}); HIDE_CATS.forEach(c => { if (c.site === SITE) delete cfgNow[c.id]; }); Store.set('bc_hide_cfg', cfgNow); setCustom([]); applyHideCSS(); this.render(); }; g.append(reset); frag.append(g); } } let HS_AV_TRIED = null, HS_AV_Q = Promise.resolve(); const histOn = () => Store.get('bc_hist_on', true) !== false; const histLoad = () => { const h = Store.get('bc_hist', {}); return { m: h && h.m && typeof h.m === 'object' && !Array.isArray(h.m) ? h.m : {}, s: h && Array.isArray(h.s) ? h.s : [] }; }; const histPos = v => (Number.isFinite(v) && v > 0 ? v : 0); const newHistModel = e => ({ n: clip(e.n, 100), u: e.u, a: toHttps(e.a), s: histPos(e.s), v: histPos(e.v), f: histPos(e.f), l: histPos(e.l) }); const updateHistModel = (c, e) => { const num = histPos; c.s = Math.max(c.s, num(e.s)); c.v = Math.max(c.v, num(e.v)); c.f = Math.min(c.f || Infinity, num(e.f) || Infinity); if (!Number.isFinite(c.f)) c.f = 0; if (num(e.l) > c.l) { c.l = num(e.l); c.n = clip(e.n, 100); c.u = e.u; if (e.a) c.a = toHttps(e.a); } }; const mergeHistModel = (h, k, e) => { if (!e || !isStr(e.n) || !isStr(e.u)) return; const c = h.m[k]; if (c) updateHistModel(c, e); else h.m[k] = newHistModel(e); }; const mergeHistSessions = (h, list) => { const seen = new Set(h.s.map(x => `${x.k}|${x.t}`)); (Array.isArray(list) ? list : []).forEach(x => { if (!x || !isStr(x.k) || !histPos(x.t) || seen.has(`${x.k}|${x.t}`)) return; h.s.push({ k: x.k, n: clip(x.n, 100), t: x.t, d: histPos(x.d) }); }); }; const histMerge = src => { const h = histLoad(); const m = src.m && typeof src.m === 'object' && !Array.isArray(src.m) ? src.m : {}; Object.keys(m).forEach(k => mergeHistModel(h, k, m[k])); mergeHistSessions(h, src.s); h.s.sort((a, b) => a.t - b.t); Store.set('bc_hist', h); }; const fmtDur = sec => { const s = Math.max(0, Math.round(sec) || 0), h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), p = n => String(n).padStart(2, '0'); return h ? `${h}:${p(m)}:${p(s % 60)}` : `${m}:${p(s % 60)}`; }; const fmtStamp = ts => { if (!ts) return ''; const d = new Date(ts), p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; }; const csvCell = v => { let s = String(v ?? ''); if (s[0] === '-' && s.length <= 250 && !/[\r\n\t]/.test(s)) { return `"${`="${s.replace(/"/g, '""')}"`.replace(/"/g, '""')}"`; } if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`; return `"${s.replace(/"/g, '""')}"`; }; const csvText = rows => `\ufeff${rows.map(r => r.map(csvCell).join(',')).join('\r\n')}`; const askConfirm = (ui, text, onYes) => { const ov = mk('div', 'cf-ov'), card = mk('div', 'cf-card'), btns = mk('div', 'cf-btns'); const unbindEsc = bindEscape(() => close()); const close = () => { ov.remove(); unbindEsc(); }; const no = textEl('', L().cf_cancel, 'button'), yes = textEl('cf-yes', L().cf_ok, 'button'); no.onclick = close; yes.onclick = () => { close(); onYes(); }; btns.append(no, yes); card.append(textEl('cf-txt', text), btns); card.onclick = e => e.stopPropagation(); ov.onclick = close; ov.append(card); ui.shadow.appendChild(ov); }; const tagRows = (m, pim) => [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([v, n]) => ({ v, t: v, n, ic: ptImg(pim.get(v), 'cal-tic') })); class CalendarManager { constructor(ui, bm) { this.ui = ui; this.bm = bm; const now = new Date(); this.y = now.getFullYear(); this.m = now.getMonth(); this.sel = this.dkey(now); Object.assign(this, CalendarManager.loadPrefs()); this.pick = false; this.dd = null; this.wOpen = false; this.pane = mk('div', 'cal-pane'); this.bm.modal.appendChild(this.pane); this.sk = ''; this.sn = 0; setInterval(() => this.scan(), 8000); onRemoteChange('bc_hist', () => { if (this.isOpen()) this.render(); }); } static loadPrefs() { const lay = Store.get('bc_cal_lay', 3); const wf = Store.get('bc_cal_wf', -1); const ms = Store.get('bc_cal_models', []); return { lay: Number.isInteger(lay) && lay >= 0 && lay <= 3 ? lay : 3, wf: [0, 1].includes(wf) ? wf : null, grp: Store.get('bc_cal_grp', '') || '', tag: Store.get('bc_cal_tag', '') || '', models: new Set(Array.isArray(ms) ? ms.filter(isStr) : []) }; } scan() { const key = pageKey(); if (!key || realHidden()) return; if (this.sk !== key) { this.sk = key; this.sn = 0; } if (this.sn >= 6) return; this.sn++; if (this.bm.scanPage()) this.sn = 99; } isOpen() { return this.bm.overlay.classList.contains('open') && this.bm.mode === 'cal'; } show() { this.render(); this.bm.checkStatuses().then(() => { if (this.isOpen()) this.render(); }).catch(() => {}); (async () => { for (let i = 0; i < 60 && this.isOpen(); i++) { const r = await this.bm.bgExtra(true); if (r === 'none') break; if (r === 'busy') { await new Promise(res => setTimeout(res, 1500)); continue; } if (r && this.isOpen()) this.render(); } })(); } dkey(d) { const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; } fmt(opts, d) { let s; try { s = new Intl.DateTimeFormat((AppState.lang || 'en').toLowerCase(), opts).format(d); } catch { s = new Intl.DateTimeFormat('en', opts).format(d); } return s; } cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; } dur(sec) { const l = L(); if (sec < 60) return fmtDur(sec); const m = Math.round(sec / 60), h = Math.floor(m / 60); return h ? `${h} ${l.u_h} ${m % 60} ${l.u_m}` : `${m} ${l.u_m}`; } ib(cls, svg, text, tag = 'button') { const e = mk(tag, cls); e.innerHTML = text ? `${svg}<span>${escHTML(text)}</span>` : svg; return e; } span(x) { const m = String(x).match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})/); if (!m) return [0, 0]; const s = +m[1] * 60 + +m[2]; let e = +m[3] * 60 + +m[4]; if (e <= s) e = 1440; return [s, e]; } hm(min) { const p = n => String(n).padStart(2, '0'); return `${p(Math.floor(min / 60))}:${p(min % 60)}`; } rangesText(r, l) { return r.split(',').map(x => { const [s, e] = this.span(x); return s === 0 && e >= 1440 ? l.cal_allday : `${this.hm(s)}\u2013${this.hm(e)}`; }).join(' \u00b7 '); } bar(r, nowMin) { const bar = mk('div', 'cal-bar'); r.split(',').forEach(x => { const [s, e] = this.span(x); if (e <= s) return; const seg = mk('i', 'cal-seg'); seg.style.left = `${s / 14.4}%`; seg.style.width = `${Math.max((e - s) / 14.4, 1)}%`; bar.append(seg); }); if (nowMin != null) { const n = mk('i', 'cal-now'); n.style.left = `${nowMin / 14.4}%`; bar.append(n); } return bar; } startMin(r) { const m = String(r).match(/^(\d{1,2}):(\d{2})/); return m ? +m[1] * 60 + +m[2] : 0; } favs(hist) { return AppState.bookmarks.filter(b => isObj(b) && isStr(b.url) && (this.wf == null || bmWatch(b, hist) === this.wf) && (!this.grp || b.g === this.grp) && bmTagMatch(b, this.tag) && (!this.models.size || this.models.has(bmKey(b.url)))); } filtersOn() { return this.wf != null || !!this.grp || !!this.tag || this.models.size > 0; } collect() { const hist = histLoad(); const favs = this.favs(hist), fk = new Set(favs.map(b => bmKey(b.url))); const on = this.filtersOn(); const sess = (this.lay & 1) ? this.groupSessions(hist, on, fk) : new Map(); const sched = (this.lay & 2) ? this.buildSched(favs) : [[], [], [], [], [], [], []]; const anySched = AppState.bookmarks.some(b => Array.isArray(b.sch) && b.sch.some(x => this.clean(x))); return { hist, sess, sched, anySched }; } groupSessions(hist, on, fk) { const sess = new Map(); for (const s of hist.s) { if (on && !fk.has(s.k)) continue; const dk = this.dkey(new Date(s.t)); if (!sess.has(dk)) sess.set(dk, []); sess.get(dk).push(s); } return sess; } buildSched(favs) { const sched = [[], [], [], [], [], [], []]; for (const b of favs) if (Array.isArray(b.sch)) b.sch.forEach((x, i) => { const r = this.clean(x); if (r && i < 7) sched[i].push({ b, r, s: this.startMin(r) }); }); sched.forEach(a => a.sort((x, y) => x.s - y.s)); return sched; } clean(r) { return String(r || '').split(',').filter(x => { const m = x.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})/); return m && !(+m[1] === +m[3] && m[2] === m[4]); }).join(','); } shift(d) { const x = new Date(this.y, this.m + d, 1); this.y = x.getFullYear(); this.m = x.getMonth(); this.render(); } save(k, v) { Store.set(k, v); this.render(); } render() { const l = L(), { hist, sess, sched, anySched } = this.collect(); const pane = this.pane, keepTop = pane.scrollTop; pane.replaceChildren(); const head = this.renderHead(l); const filters = this.renderFilters(l, hist); const now = new Date(), tk = this.dkey(now); const dock = mk('div', 'cal-dock'); dock.append(head, this.dockLayers); pane.append(filters, this.renderGrid(sess, sched, now, tk), this.renderDetail(l, hist, sess, sched, anySched, now, tk), dock); pane.scrollTop = keepTop; } renderHead(l) { const head = mk('div', 'cal-head'); const prev = this.ib('cal-nav', icons.chevL, ''), next = this.ib('cal-nav', icons.chevR, ''); prev.onclick = () => this.shift(-1); next.onclick = () => this.shift(1); const today = textEl('cal-today', l.cal_today, 'button'); today.onclick = () => { const n = new Date(); this.y = n.getFullYear(); this.m = n.getMonth(); this.sel = this.dkey(n); this.render(); }; head.append(prev, textEl('cal-title', this.cap(this.fmt({ month: 'long', year: 'numeric' }, new Date(this.y, this.m, 1)))), next, today); return head; } filterRow1(l) { const r1 = mk('div', 'cal-frow'); const layers = mk('div', 'bm-seg'); [[1, icons.eye, l.cal_hist], [2, icons.clock, l.cal_sched]].forEach(([bit, ic, t]) => { const b = this.ib(`bm-tbtn${this.lay & bit ? ' on' : ''}`, ic, t); b.onclick = () => { const v = this.lay ^ bit; this.lay = v; this.save('bc_cal_lay', v); }; layers.append(b); }); const wseg = mk('div', 'bm-seg'); WMARKS.forEach(([v, ic, key]) => { const b = this.ib(`bm-tbtn${this.wf === v ? ' on' : ''}`, icons[ic], l[key]); b.title = l[key]; b.onclick = () => { this.wf = this.wf === v ? null : v; this.save('bc_cal_wf', this.wf == null ? -1 : this.wf); }; wseg.append(b); }); this.dockLayers = layers; r1.append(wseg); return r1; } mkDD(kind, icon, label, on) { const b = this.ib(`cal-dd${on ? ' on' : ''}${this.dd === kind ? ' open' : ''}`, icon, label); b.onclick = () => { this.dd = this.dd === kind ? null : kind; this.pick = false; this.render(); }; return b; } mkPanel(opts, cur, set) { const box = mk('div', 'cal-panel'); opts.forEach(o => { if (o.sep) { box.append(mk('div', 'cal-sep')); return; } const c = this.ib(`cal-opt${o.v === cur ? ' on' : ''}${o.all ? ' all' : ''}`, o.ic || '', o.t); if (o.n) c.insertAdjacentHTML('beforeend', `<i>${o.n}</i>`); c.onclick = () => { this.dd = null; set(o.v); }; box.append(c); }); return box; } grpDropdown(l, r2) { const groups = bmGroups(); if (this.grp && !groups.includes(this.grp)) this.grp = ''; if (!groups.length) return null; r2.append(this.mkDD('grp', icons.folder, this.grp || l.grp_all, !!this.grp)); if (this.dd !== 'grp') return null; return this.mkPanel([{ v: '', t: l.grp_all, all: true }, ...groups.map(g => ({ v: g, t: g }))], this.grp, v => { this.grp = v; this.save('bc_cal_grp', v); }); } countTags(hidePT) { const noteTags = new Map(), profTags = new Map(); AppState.bookmarks.forEach(b => { noteHashTags(b).forEach(x => countInto(noteTags, x)); if (!hidePT) (b.ptags || []).forEach(x => countInto(profTags, x)); }); return { noteTags, profTags }; } tagDropdown(l, r2) { const hidePT = Store.get('bc_bm_hide_ptags', true) !== false; const pim = ptIcoMap(); const { noteTags, profTags } = this.countTags(hidePT); if (this.tag && !noteTags.has(this.tag) && !profTags.has(this.tag)) this.tag = ''; if (!(noteTags.size + profTags.size)) return null; r2.append(this.mkDD('tag', ptImg(pim.get(this.tag), 'cal-tic') || icons.tag, this.tag || l.cal_all_tag, !!this.tag)); if (this.dd !== 'tag') return null; return this.mkPanel(this.tagItems(l, noteTags, profTags, pim), this.tag, v => { this.tag = v; this.save('bc_cal_tag', v); }); } tagItems(l, noteTags, profTags, pim) { const srt = m => tagRows(m, pim); const sep = noteTags.size && profTags.size ? [{ sep: true }] : []; return [{ v: '', t: l.cal_all_tag, all: true }, ...srt(noteTags), ...sep, ...srt(profTags)]; } modelPicker(l, hist) { const box = mk('div', 'cal-models'); if (this.models.size) { const rs = this.ib('cal-mrow reset', icons.undo, l.cal_reset); rs.onclick = () => { this.models.clear(); this.save('bc_cal_models', []); }; box.append(rs); } const nk = s => String(s).replace(/^[^\p{L}\p{N}]+/u, ''); AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)).slice() .sort((a, b) => nk(a.name).localeCompare(nk(b.name), undefined, { sensitivity: 'base', numeric: true })) .forEach(b => { const k = bmKey(b.url), on = this.models.has(k); const r = mk('button', `cal-mrow${on ? ' on' : ''}`); const img = mk('img', 'cal-ava sm'); bindAvatar(img, b.name, () => [toHttps(b.avatar), toHttps(hist.m[k]?.a)]); r.append(img, textEl('', b.name, 'span')); if (on) r.insertAdjacentHTML('beforeend', icons.check); r.onclick = () => { if (on) this.models.delete(k); else this.models.add(k); this.save('bc_cal_models', [...this.models]); }; box.append(r); }); return box; } renderFilters(l, hist) { const flt = mk('div', 'cal-filters'); const r2 = mk('div', 'cal-frow'); const grpPanel = this.grpDropdown(l, r2); const tagPanel = this.tagDropdown(l, r2); const ddPanel = tagPanel || grpPanel; const r1 = this.filterRow1(l); flt.append(r1); if (r2.children.length) flt.append(r2); if (ddPanel) flt.append(ddPanel); const mb = textEl(`cal-pill${this.models.size ? ' on' : ''}`, `${l.cal_models}${this.models.size ? ` (${this.models.size})` : ''}`, 'button'); mb.onclick = () => { this.pick = !this.pick; this.dd = null; this.render(); }; r1.append(mb); if (this.pick) flt.append(this.modelPicker(l, hist)); return flt; } renderGrid(sess, sched, now, tk) { const grid = mk('div', 'cal-grid'); for (let i = 0; i < 7; i++) grid.append(textEl('cal-wd', this.fmt({ weekday: 'short' }, new Date(2024, 0, 1 + i)))); const first = new Date(this.y, this.m, 1), off = (first.getDay() + 6) % 7, dim = new Date(this.y, this.m + 1, 0).getDate(); const t0 = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); for (let i = 0; i < off; i++) grid.append(mk('div', 'cal-day blank')); for (let d = 1; d <= dim; d++) grid.append(this.dayCell(d, sess, sched, tk, t0)); return grid; } dayCell(d, sess, sched, tk, t0) { const dt = new Date(this.y, this.m, d), key = this.dkey(dt), wd = (dt.getDay() + 6) % 7; const cell = mk('button', `cal-day${key === tk ? ' today' : ''}${key === this.sel ? ' sel' : ''}${dt.getTime() < t0 ? ' past' : ''}`); cell.type = 'button'; cell.append(textEl('cal-n', String(d), 'span')); const bs = mk('div', 'cal-bs'), sn = new Set((sess.get(key) || []).map(s => s.k)).size, sc = sched[wd].length; if (sn) bs.append(textEl('cal-b', String(sn), 'i')); if (sc) bs.append(textEl('cal-b s', String(sc), 'i')); cell.append(bs); cell.onclick = () => { this.sel = key; this.render(); }; return cell; } watchedBox(l, hist, sess, ctx) { const wbox = mk('div', 'cal-wsec'); const agg = new Map(); (sess.get(this.sel) || []).forEach(s => { const e = agg.get(s.k) || { k: s.k, n: s.n, first: s.t, d: 0, c: 0 }; e.d += s.d || 0; e.c++; e.first = Math.min(e.first, s.t); agg.set(s.k, e); }); const list = [...agg.values()].sort((a, b) => a.first - b.first); const total = list.reduce((s, x) => s + x.d, 0); const fh = this.ib(`cal-sec cal-fold${this.wOpen ? ' open' : ''}`, '', `${l.cal_watched}${total ? ` · ${this.dur(total)}` : ''}`); fh.onclick = () => { this.wOpen = !this.wOpen; this.render(); }; wbox.append(fh); if (!this.wOpen) return wbox; if (!list.length) wbox.append(textEl('cal-muted', l.cal_none)); else wbox.append(this.watchedTable(l, hist, list, total, ctx)); return wbox; } watchedTable(l, hist, list, total, { favMap, href, ava }) { const p2 = n => String(n).padStart(2, '0'); const tbl = mk('div', 'cal-tbl'); const hd = mk('div', 'cal-trow cal-thead'); [l.cal_c_model, l.cal_c_time, l.cal_c_dur, l.cal_c_n].forEach(x => hd.append(textEl('', x, 'span'))); tbl.append(hd); list.forEach(e => { const b = favMap.get(e.k), row = mk('div', 'cal-trow'), name = b ? b.name : e.n; const who = mk('div', 'cal-who'); const a = textEl('', name, 'a'); a.href = href(b ? b.url : hist.m[e.k]?.u); who.append(ava(b, e.k, name), a); const t = new Date(e.first); row.append(who, textEl('', `${p2(t.getHours())}:${p2(t.getMinutes())}`, 'span'), textEl('', this.dur(e.d), 'span'), textEl('', `×${e.c}`, 'span')); tbl.append(row); }); const ft = mk('div', 'cal-trow cal-tfoot'); ft.append(textEl('', `${l.cal_total} · ${list.length}`, 'span'), textEl('', '', 'span'), textEl('', this.dur(total), 'span'), textEl('', `×${list.reduce((s, x) => s + x.c, 0)}`, 'span')); tbl.append(ft); return tbl; } schedRow(l, b, r, nowMin, { href, ava }) { const row = mk('div', 'cal-row'), line = mk('div', 'cal-line'), body = mk('div', 'cal-body'); if (nowMin != null && (b.status ?? 0) > 0) line.append(mk('span', 'cal-dot')); const a = textEl('', b.name, 'a'); a.href = href(b.url); line.append(a, textEl('cal-t', this.rangesText(r, l), 'span')); body.append(line, this.bar(r, nowMin)); row.append(ava(b, bmKey(b.url), b.name), body); return row; } appendSched(det, l, list, anySched, nowMin, ctx) { det.append(textEl('cal-sec', l.cal_sched)); if (!list.length) det.append(textEl('cal-muted', anySched ? l.cal_none : l.cal_no_sched)); else { const ax = mk('div', 'cal-axis'); ['00', '06', '12', '18', '24'].forEach(h => ax.append(textEl('', h, 'span'))); det.append(ax); } list.forEach(({ b, r }) => det.append(this.schedRow(l, b, r, nowMin, ctx))); const bc = AppState.bookmarks.filter(b => isObj(b) && isStr(b.url) && urlSite(b.url) === 'bc'); const got = bc.filter(b => Array.isArray(b.sch)).length; if (bc.length && got < bc.length) det.append(textEl('cal-muted', l.cal_loaded.replace('{a}', got).replace('{b}', bc.length))); } renderDetail(l, hist, sess, sched, anySched, now, tk) { const sp = this.sel.split('-').map(Number), sd = new Date(sp[0], sp[1] - 1, sp[2]), swd = (sd.getDay() + 6) % 7; const det = mk('div', 'cal-det'); det.append(textEl('cal-dtitle', this.cap(this.fmt({ weekday: 'long', day: 'numeric', month: 'long' }, sd)))); const favMap = new Map(AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)).map(b => [bmKey(b.url), b])); const href = u => (u && isSafeUrl(u) ? linkFor(u) : '#'); const ava = (b, k, name) => { const img = mk('img', 'cal-ava'); bindAvatar(img, name, () => [toHttps(b?.avatar), toHttps(hist.m[k]?.a)]); return img; }; const ctx = { favMap, href, ava }; const wbox = (this.lay & 1) ? this.watchedBox(l, hist, sess, ctx) : null; if (this.lay & 2) { const nowMin = this.sel === tk ? now.getHours() * 60 + now.getMinutes() : null; this.appendSched(det, l, sched[swd], anySched, nowMin, ctx); } if (wbox) det.append(wbox); return det; } } class HistoryManager { constructor(ui, bookmarks) { this.ui = ui; this.bm = bookmarks; this.cur = null; this.pend = 0; this.fresh = false; this.lastTick = Date.now(); const sort = Store.get('bc_hist_sort', 'last'); this.sort = ['last', 'time', 'visits'].includes(sort) ? sort : 'last'; this.build(); setInterval(() => this.tick(), 5000); onPageEvent('pagehide', () => this.endSession(), window); onPageEvent('visibilitychange', () => { if (realHidden()) this.flush(); }); onRemoteChange('bc_hist', () => { if (this.isOpen()) this.render(); }); } build() { this.list = mk('div', 'hs-pane'); this.bm.modal.appendChild(this.list); } isOpen() { return this.bm.overlay.classList.contains('open') && this.bm.mode === 'hist'; } show() { this.flush(); this.render(); } open() { this.bm.open('hist'); } hide() { this.bm.hide(); } updateLabels() { if (this.isOpen()) this.render(); } tick() { const now = Date.now(), dt = Math.min(65, (now - this.lastTick) / 1000); this.lastTick = now; if (!histOn()) return; const key = pageKey(), name = key ? currentSite.getName() : ''; if (!key || !name) { this.endSession(); return; } this.ensureSession(key, name, now); if (isPlaying(selectorVideo())) this.pend += dt; if (this.pend >= 15) this.flush(); } ensureSession(key, name, now) { if (!this.cur || this.cur.k !== key) { this.endSession(); this.cur = { k: key, t: now, name, url: normUrl(location.href), avatar: '' }; this.fresh = true; } this.cur.name = name; if (!this.cur.avatar) this.cur.avatar = toHttps(pageAvatar(name)) || AppState.bookmarks.find(b => bmKey(b.url) === key)?.avatar || ''; } flush() { const c = this.cur, secs = Math.round(this.pend); if (!c || secs < 1) return; this.pend = 0; const h = histLoad(); this.recordSession(h, c, secs, Date.now()); Store.set('bc_hist', h); if (this.isOpen()) this.render(); } recordSession(h, c, secs, now) { const e = h.m[c.k] || (h.m[c.k] = { n: c.name, u: c.url, a: '', s: 0, v: 0, f: c.t, l: c.t }); e.n = c.name; e.u = c.url; if (c.avatar) e.a = c.avatar; e.s += secs; e.l = now; if (this.fresh) { e.v++; this.fresh = false; } let s = h.s.find(x => x.k === c.k && x.t === c.t); if (!s) { s = { k: c.k, n: c.name, t: c.t, d: 0 }; h.s.push(s); } s.d += secs; } endSession() { this.flush(); this.cur = null; this.pend = 0; } remove(k) { const h = histLoad(); delete h.m[k]; h.s = h.s.filter(x => x.k !== k); Store.set('bc_hist', h); if (this.cur?.k === k) { this.cur = null; this.pend = 0; } this.render(); } clear() { askConfirm(this.ui, L().hist_clear_q, () => { Store.set('bc_hist', { m: {}, s: [] }); this.cur = null; this.pend = 0; this.render(); }); } exportCsv(sessions) { const h = histLoad(); const rows = sessions ? [['name', 'url', 'start', 'seconds', 'duration'], ...h.s.slice().sort((a, b) => b.t - a.t).map(x => [x.n, h.m[x.k]?.u || '', fmtStamp(x.t), x.d, fmtDur(x.d)])] : [['name', 'url', 'seconds', 'duration', 'visits', 'first_seen', 'last_seen'], ...Object.keys(h.m).map(k => h.m[k]).sort((a, b) => b.l - a.l).map(e => [e.n, e.u, e.s, fmtDur(e.s), e.v, fmtStamp(e.f), fmtStamp(e.l)])]; Helpers.download(new Blob([csvText(rows)], { type: 'text/csv;charset=utf-8' }), `BC_${sessions ? 'Sessions' : 'History'}_${Helpers.getTimestamp().slice(0, 10)}.csv`); } loadAvatar(e, img) { HS_AV_TRIED ||= new Set(); if (!e.u || HS_AV_TRIED.has(e.k)) return; HS_AV_TRIED.add(e.k); HS_AV_Q = HS_AV_Q.then(async () => { const av = extractAvatar((await withBackoff(() => requestOnce(e.u))) || ''); if (!av) return; const h = histLoad(); if (h.m[e.k]) { h.m[e.k].a = av; Store.set('bc_hist', h); } if (img.isConnected) bindAvatar(img, e.n || '?', () => [av]); }).catch(() => {}); } row(e, l) { const row = mk('div', 'hs-row'); const img = mk('img', 'hs-ava'); bindAvatar(img, e.n || '?', () => { const bmk = AppState.bookmarks.find(x => bmKey(x.url) === e.k); return [toHttps(e.a), toHttps(bmk?.avatar)]; }, () => this.loadAvatar(e, img)); const info = mk('div', 'hs-info'); info.append(textEl('hs-name', e.n || e.k), textEl('hs-meta', `${fmtDur(e.s)} · ${e.v} ${l.hist_visits} · ${fmtAgo(e.l)}`)); const fav = isFavKey(e.k); const favBtn = mk('button', `hs-btn${fav ? ' on' : ''}`); favBtn.innerHTML = icons.star; favBtn.title = fav ? l.star_del : l.star_add; favBtn.onclick = ev => { ev.stopPropagation(); if (fav) return this.bm.removeKey(e.k, () => this.render()); this.bm.addEntry({ name: e.n, url: e.u, avatar: e.a }); this.render(); }; const delBtn = mk('button', 'hs-btn del'); delBtn.innerHTML = icons.del; delBtn.title = l.hist_del; delBtn.onclick = ev => { ev.stopPropagation(); this.remove(e.k); }; row.onclick = () => { if (isSafeUrl(e.u)) location.href = linkFor(e.u); }; row.append(img, info, favBtn, delBtn); return row; } vbars(vals, labels, tips) { const max = Math.max(...vals, 1), bars = mk('div', 'ch-bars'), axis = mk('div', 'ch-axis'); vals.forEach((v, i) => { const col = mk('div', 'ch-col'), bar = mk('div', `ch-bar${v > 0 ? '' : ' zero'}`); bar.style.height = `${v > 0 ? Math.max(3, Math.round(v / max * 100)) : 0}%`; col.title = tips[i]; col.append(bar); bars.append(col); axis.append(textEl('', labels[i], 'span')); }); return [bars, axis]; } chartBuckets(h) { const DAY = 864e5, HOUR = 36e5, now = Date.now(); const base = new Date(); base.setHours(0, 0, 0, 0); const start0 = base.getTime() - 13 * DAY; const days = new Array(14).fill(0), hours = new Array(24).fill(0); h.s.forEach(x => { let a = x.t, rem = x.d * 1000; while (rem > 0 && a <= now + DAY) { const dt = new Date(a), hs = new Date(a); hs.setMinutes(0, 0, 0); const part = Math.min(rem, hs.getTime() + HOUR - a); hours[dt.getHours()] += part; dt.setHours(0, 0, 0, 0); const di = Math.round((dt.getTime() - start0) / DAY); if (di >= 0 && di < 14) days[di] += part; a += part; rem -= part; } }); return { days, hours, start0 }; } chartTop(box, entries, l) { const top = entries.slice().sort((x, y) => y.s - x.s).slice(0, 5); if (!top.length) return null; const b3 = box(l.ch_top), tMax = Math.max(top[0].s, 1); top.forEach(e => { const row = mk('div', 'ch-row'), track = mk('div', 'ch-track'), fill = mk('div', 'ch-fill'); fill.style.width = `${Math.max(2, Math.round(e.s / tMax * 100))}%`; track.append(fill); row.append(textEl('ch-name', e.n || e.k, 'span'), track, textEl('ch-val', fmtDur(e.s), 'span')); b3.append(row); }); return b3; } charts(h, entries, l) { const DAY = 864e5; const { days, hours, start0 } = this.chartBuckets(h); const sec = ms => Math.round(ms / 1000); const box = (title, extra) => { const b = mk('div', 'ch-box'), t = mk('div', 'ch-ttl'); t.append(textEl('', title, 'span')); if (extra) t.append(textEl('', extra, 'span')); b.append(t); return b; }; const wrap = document.createDocumentFragment(); const dayLbl = days.map((_, i) => new Date(start0 + i * DAY + DAY / 2).getDate()); const dayTip = days.map((v, i) => `${this.dayName(start0 + i * DAY + DAY / 2)}: ${fmtDur(sec(v))}`); const avg = days.reduce((a, v) => a + v, 0) / 14; const b1 = box(l.ch_days, `${l.ch_avg}: ${fmtDur(sec(avg))}`); b1.append(...this.vbars(days, dayLbl, dayTip)); wrap.append(b1); const hMax = Math.max(...hours); const peak = hMax > 0 ? hours.indexOf(hMax) : -1; const hrLbl = hours.map((_, i) => (i % 6 === 0 ? String(i) : '')); const hrTip = hours.map((v, i) => `${String(i).padStart(2, '0')}:00: ${fmtDur(sec(v))}`); const b2 = box(l.ch_hours, peak >= 0 ? `${l.ch_peak}: ${String(peak).padStart(2, '0')}:00` : ''); b2.append(...this.vbars(hours, hrLbl, hrTip)); wrap.append(b2); const b3 = this.chartTop(box, entries, l); if (b3) wrap.append(b3); return wrap; } dayName(ts) { try { return new Date(ts).toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return ''; } } render() { const l = L(), h = histLoad(), keep = this.list.scrollTop; const entries = Object.keys(h.m).map(k => ({ k, ...h.m[k] })); const total = entries.reduce((a, e) => a + e.s, 0); const cmps = { last: (x, y) => y.l - x.l, time: (x, y) => y.s - x.s, visits: (x, y) => (y.v - x.v) || (y.s - x.s) }; entries.sort(cmps[this.sort]); const frag = document.createDocumentFragment(); const sorts = mk('div', 'st-chips hs-seg'); [['last', l.hist_s_last], ['time', l.hist_s_time], ['visits', l.hist_s_vis]].forEach(([id, t]) => { const b = textEl(`bm-tbtn${this.sort === id ? ' on' : ''}`, t, 'button'); b.onclick = () => { this.sort = id; Store.set('bc_hist_sort', id); this.render(); }; sorts.append(b); }); const chOn = Store.get('bc_hist_ch', false) === true; const chBtn = textEl(`bm-tbtn${chOn ? ' on' : ''}`, l.ch_btn, 'button'); chBtn.onclick = () => { Store.set('bc_hist_ch', !chOn); this.render(); }; sorts.append(chBtn); const clr = mk('button', 'hs-trash'); clr.innerHTML = icons.del; clr.title = l.hist_clear; clr.setAttribute('aria-label', l.hist_clear); clr.onclick = () => this.clear(); const top = mk('div', 'st-chips hs-top'); top.append(textEl('hd-hint', l.hist_total.replace('{n}', entries.length).replace('{t}', fmtDur(total))), clr); frag.append(sorts, top); if (!entries.length) frag.append(textEl('bm-empty', l.hist_empty)); else if (chOn) frag.append(this.charts(h, entries, l)); entries.slice(0, 300).forEach(e => frag.append(this.row(e, l))); this.list.replaceChildren(frag); this.list.scrollTop = keep; } } const STAR_SVG = '<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'; const THUMB_CFG = { bc: { links: ['a.thm_a', 'a.lst_link'], box: '.mls_item, .ls_thumb', views: '.bage_viewers, .lst_viewers', name: '' }, sc: { links: ['a[data-model-list-link="true"]'], box: '[data-testid="model-list-item"]', views: '[class*="iewers"]', name: '[data-testid="model-list-item-username"]' }, cb: { links: ['a.RoomCardThumbnail', 'a[data-testid="room-card-image-anchor"]'], box: '.RoomCard, li.room_list_room, li.roomCard', views: '.viewers', name: '[data-testid="room-card-username"], .username a' } }[SITE] || { links: [], box: '', views: '', name: '' }; const THUMB_LINKS = THUMB_CFG.links; const THUMB_SEL = THUMB_LINKS.join(','); const THUMB_BOX = THUMB_CFG.box; const viewersOf = a => { const t = THUMB_CFG.views ? ((THUMB_BOX && a.closest(THUMB_BOX)) || a).querySelector(THUMB_CFG.views)?.textContent || '' : ''; const n = parseCount(t); return Number.isFinite(n) ? n : null; }; class ThumbStars { constructor(bookmarks) { this.bm = bookmarks; this.on = Store.get('bc_star', true) !== false; this.timer = 0; const st = document.createElement('style'); st.id = 'bcu-star-css'; st.textContent = STAR_CSS; (document.head || document.documentElement).appendChild(st); this.bindGlobal(); const resync = () => { try { syncBookmarks(); } catch (e) { dbg(e, 'ThumbStars.resync'); } this._idx = null; this.scan(); }; window.addEventListener('pageshow', e => { if (e.persisted) resync(); }); onPageEvent('visibilitychange', () => { if (!realHidden()) resync(); }); DomBus.subscribe((recs, isOwn) => { if (!isOwn()) this.schedule(); }); setInterval(() => { if (!realHidden()) this.scan(); if (!realHidden()) this.checkPage(); }, 6000); this.schedule(); } checkPage() { const key = pageKey(); if (!key || !isFavKey(key)) return; const v = document.querySelector('video'); if (v && isPlaying(v)) this.bm.markLive(key); } bindGlobal() { const starOf = e => (e.target && e.target.closest ? e.target.closest('.bcu-star') : null); const opt = { capture: true, passive: false }; let down = null; const fire = star => { const a = star.parentElement; if (a) this.toggle(a); }; const block = e => { e.stopImmediatePropagation(); e.stopPropagation(); if (e.cancelable) e.preventDefault(); }; ['mousedown', 'mouseup', 'dblclick', 'auxclick', 'contextmenu'].forEach(t => window.addEventListener(t, e => { if (starOf(e)) block(e); }, opt)); ['touchstart', 'touchend', 'touchmove'].forEach(t => window.addEventListener(t, e => { if (starOf(e)) { e.stopImmediatePropagation(); e.stopPropagation(); } }, { capture: true, passive: true })); window.addEventListener('pointerdown', e => { const st = starOf(e); if (!st) return; down = { st, x: e.clientX, y: e.clientY }; block(e); }, opt); window.addEventListener('pointerup', e => { const st = starOf(e); if (!st) return; block(e); const ok = down && down.st === st && Math.abs(e.clientX - down.x) < 12 && Math.abs(e.clientY - down.y) < 12; down = null; if (!ok || (e.button != null && e.button > 0)) return; st._t = Date.now(); fire(st); }, opt); window.addEventListener('click', e => { const st = starOf(e); if (!st) return; block(e); if (Date.now() - (st._t || 0) > 800) fire(st); }, opt); } schedule() { if (this.timer || !this.on) return; this.timer = setTimeout(() => { this.timer = 0; this.scan(); }, 350); } setEnabled(v) { this.on = !!v; Store.set('bc_star', this.on); this._idx = null; if (this.on) return this.scan(); document.querySelectorAll('.bcu-star,.bcu-ring').forEach(x => x.remove()); THUMB_LINKS.forEach(sel => document.querySelectorAll(sel).forEach(a => { a._bcu = null; a.classList.remove('bcu-fav'); })); } scan() { if (!this.on) return; if (!THUMB_LINKS.length) return; const bms = AppState.bookmarks; if (!this._idx || this._idxRef !== bms || this._idxLen !== bms.length) { this._idx = new Map(bms.map(b => [bmKey(b.url), b])); this._idxRef = bms; this._idxLen = bms.length; } const idx = this._idx; const links = document.querySelectorAll(THUMB_SEL); const pos = Array.from(links, a => (a._bcu ? null : getComputedStyle(a).position)); links.forEach((a, i) => this.sync(a, idx, pos[i])); } keyOf(a) { const h = a.href; if (a._bcuH === h) return a._bcuK; const s = modelSlug(h); a._bcuK = s ? `${SITE}|${s.toLowerCase()}` : ''; a._bcuH = h; return a._bcuK; } sync(a, idx, pos) { const key = this.keyOf(a); if (!key) return; const st = this.ensureStar(a, key, pos); const b = idx.get(key); this.paintFav(a, st, !!b); if (b) this.syncViewers(a, key, b); } ensureStar(a, key, pos) { const st = a._bcu; if (st && st.key === key && st.star.isConnected) return st; if (st) { st.star.remove(); st.ring.remove(); } return (a._bcu = this.make(a, key, pos ?? getComputedStyle(a).position)); } paintFav(a, st, fav) { if (st.fav !== fav) { st.fav = fav; st.star.classList.toggle('on', fav); a.classList.toggle('bcu-fav', fav); } const tip = fav ? L().star_del : L().star_add; if (st.star.title !== tip) st.star.title = tip; } syncViewers(a, key, b) { const n = viewersOf(a); if (n == null) return; this.bm.markLive(key, b); if (n !== b.viewers) this.bm.setViewers(key, n, b); } make(a, key, pos) { if (pos === 'static') a.classList.add('bcu-host'); const ring = document.createElement('span'); ring.className = 'bcu-ring'; const star = document.createElement('span'); star.className = 'bcu-star'; star.setAttribute('role', 'button'); star.innerHTML = STAR_SVG; ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'touchstart', 'touchend', 'dblclick', 'contextmenu'].forEach(ev => star.addEventListener(ev, e => e.stopPropagation())); star.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); this.toggle(a); }); a.append(ring, star); return { key, star, ring, fav: null }; } thumbImg(a) { return a.querySelector('img:not([class*="Blur"])') || a.querySelector('img'); } attrName(a) { return a.getAttribute('data-name') || a.dataset.room || a.closest('[data-name]')?.getAttribute('data-name'); } boxName(a) { const box = THUMB_BOX ? a.closest(THUMB_BOX) : null; return THUMB_CFG.name && box?.querySelector(THUMB_CFG.name)?.textContent.trim(); } thumbName(a, img) { return this.attrName(a) || this.boxName(a) || (IS_BC && img?.alt) || modelSlug(a.href); } toggle(a) { const key = this.keyOf(a); if (!key) return; if (isFavKey(key)) return this.bm.removeKey(key, () => this.scan()); const img = this.thumbImg(a); this.bm.addEntry({ name: this.thumbName(a, img), url: a.href, avatar: '', viewers: viewersOf(a) }); this.scan(); } } const THEME_SWATCHES_CLASSIC = ['rgba(220, 20, 60, 0.9)', 'rgba(233, 30, 99, 0.9)', 'rgba(156, 39, 176, 0.9)', 'rgba(63, 81, 181, 0.9)', 'rgba(3, 155, 229, 0.9)', 'rgba(0, 150, 136, 0.9)', 'rgba(67, 160, 71, 0.9)', 'rgba(245, 124, 0, 0.9)', 'rgba(96, 125, 139, 0.9)']; const themeSwatches = () => THEME_SWATCHES_CLASSIC; const TILE_ICONS = { bBg: 'bgOn', bMute: 'vol', bShot: 'snap', bRec: 'rec', vol: 'volMed', bQual: 'qual', bPip: 'pip', bFs: 'fs', bThtr: 'thtr', bSearch: 'search', bHls: 'hls', bCopy: 'copy', bFav: 'fav', stats: 'eye', bHome: 'home', bRel: 'rel', bColor: 'color', bHideMgr: 'eyeOff', scale: 'grid', bDark: 'moon', bBkEx: 'dl', bBkIm: 'ul', bDon: 'coffee', hints: 'menu' }; const colorToHsl = c => { try { const s = document.createElement('span'); s.style.color = c; document.documentElement.appendChild(s); const v = getComputedStyle(s).color; s.remove(); const m = v.match(/(\d+)\D+(\d+)\D+(\d+)/); if (!m) return [348, 83, 47]; const r = m[1] / 255, g = m[2] / 255, b = m[3] / 255; const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn, l = (mx + mn) / 2; let h = 0, sat = 0; if (d) { sat = d / (1 - Math.abs(2 * l - 1)); h = mx === r ? ((g - b) / d) % 6 : mx === g ? (b - r) / d + 2 : (r - g) / d + 4; h = (h * 60 + 360) % 360; } return [Math.round(h), Math.round(sat * 100), Math.round(l * 100)]; } catch { return [348, 83, 47]; } }; const noFade = (...els) => { const list = els.filter(Boolean); list.forEach(e => e.classList.add('bm-nofade')); requestAnimationFrame(() => requestAnimationFrame(() => list.forEach(e => e.classList.remove('bm-nofade')))); }; const stBack = (ui, overlay) => { if (!overlay) return; const modal = overlay.querySelector('.bm-modal'), hdr = overlay.querySelector('.bm-hdr'); if (!modal || !hdr) return; if (!overlay._bk) { const btn = mk('button', 'bm-close bm-back'); btn.innerHTML = icons.back; btn.style.display = 'none'; const sync = () => { btn.style.display = overlay._fromSt ? 'flex' : 'none'; modal.classList.toggle('has-back', !!overlay._fromSt); btn.title = L().st_back || ''; }; btn.onclick = e => { e.stopPropagation(); overlay._fromSt = false; sync(); noFade(overlay, ui.settingsManager?.overlay); ui.settingsManager?.open(); overlay.classList.remove('open'); }; overlay.addEventListener('click', e => { if (!overlay._fromSt) return; const t = e.target; const leave = t === overlay ? Date.now() - (overlay._stAt || 0) > 400 : !!(t.closest && t.closest('.bm-close') && !t.closest('.bm-back')); if (leave) { overlay._fromSt = false; sync(); } }, true); hdr.appendChild(btn); overlay._bk = sync; } overlay._fromSt = true; overlay._stAt = Date.now(); overlay._bk(); }; const ST_CSS = `.rs-pult.st-modal .bm-list>*{flex:none} .rs-pult.st-modal .hd-hint{padding:0 4px;font-size:12px;line-height:1.4;color:#fff;opacity:.6} .rs-pult.st-modal .st-tabs{order:0;flex:none;display:flex;gap:6px;margin:8px 8px 0;padding:8px;border-radius:14px;background:rgba(0,0,0,.34)} .rs-pult.st-modal .st-tab{flex:1 1 0;min-width:0;min-height:44px;margin:0;padding:0 4px;border-radius:12px;border:1px solid var(--pl);background:var(--pt);color:#fff;font:500 13px/1.2 Onest,system-ui,sans-serif;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;transition:background .12s,transform .12s} .rs-pult.st-modal .st-tab.on{background:var(--th);border-color:transparent;color:var(--pon);font-weight:600} .rs-pult.st-modal .st-tab:active{transform:scale(.97)} .rs-pult.st-modal .st-tab:focus-visible,.rs-pult.st-modal .st-pill:focus-visible,.rs-pult.st-modal .st-tile:focus-visible,.rs-pult.st-modal .st-swatch:focus-visible{outline:2px solid var(--th);outline-offset:2px} .rs-pult.st-modal .st-row{display:flex;align-items:center;gap:10px;min-height:48px;padding:0 10px 0 12px;border-radius:12px;border:1px solid var(--pl);background:var(--pt);color:#fff;font-size:14px} .rs-pult.st-modal .st-row:has(.rs-chips){flex-wrap:wrap;padding:8px 10px 8px 12px} .rs-pult.st-modal .st-row:has(.rs-chips) .st-lbl{flex:1 0 100%} .rs-pult.st-modal .st-row .rs-chips{flex:1 1 100%} .rs-pult.st-modal .st-lbl{flex:1;min-width:0;display:flex;align-items:center;gap:10px;font-size:14px;line-height:1.25} .rs-pult.st-modal .st-ic{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;color:var(--th);filter:brightness(1.4)} .rs-pult.st-modal .st-ic svg{display:block;width:20px;height:20px;margin:0!important} .rs-pult.st-modal .st-pill{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:36px;margin:0;padding:0 12px;border-radius:10px;border:1px solid var(--pl);background:var(--pb);color:#fff;font:500 13px/1.2 Onest,system-ui,sans-serif;cursor:pointer;transition:background .12s,transform .12s} .rs-pult.st-modal .st-pill:hover{background:color-mix(in srgb,var(--pb) 85%,#fff)} .rs-pult.st-modal .st-pill:active{transform:scale(.97)} .rs-pult.st-modal .st-pill svg{display:block;width:18px;height:18px;margin:0!important} .rs-pult.st-modal .st-wide{display:flex;width:100%;min-height:44px;border-radius:12px;font-size:14px} .rs-pult.st-modal .rs-chips .st-pill{flex:1 1 auto;min-height:44px;border-radius:12px;background:var(--pt)} .rs-pult.st-modal .st-row .bm-sw{position:relative;flex:none;width:46px;height:26px;padding:0;border:0;border-radius:13px;background:rgba(255,255,255,.18);cursor:pointer} .rs-pult.st-modal .st-row .bm-sw::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff} .rs-pult.st-modal .st-row .bm-sw.on{background:var(--th)} .rs-pult.st-modal .st-row .bm-sw.on::after{left:23px} .rs-pult.st-modal .st-block{display:flex;flex-direction:column;gap:10px;padding:10px 12px;border-radius:12px;border:1px solid var(--pl);background:var(--pt);color:#fff;font-size:14px} .rs-pult.st-modal .st-sw-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px} .rs-pult.st-modal .st-swatch{flex:none;width:24px;height:24px;margin:0;padding:0;border-radius:50%;border:2px solid transparent;cursor:pointer} .rs-pult.st-modal .st-swatch.sel{border-color:#fff} .rs-pult.st-modal .st-swatch.st-custom{background:conic-gradient(#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)} .rs-pult.st-modal .st-hsl{display:flex;flex-direction:column;gap:12px;padding-top:4px} .rs-pult.st-modal .st-hrow{display:flex;flex-direction:column;gap:6px;font-size:11px;opacity:.8} .rs-pult.st-modal .st-hsl input,.rs-pult.st-modal .st-range{-webkit-appearance:none;appearance:none;margin:0;outline:none;border-radius:6px} .rs-pult.st-modal .st-hsl input{width:100%;height:14px} .rs-pult.st-modal .st-range{flex:1.5 1 0;min-width:60px;height:6px;background:rgba(255,255,255,.18)} .rs-pult.st-modal .st-hsl input::-webkit-slider-thumb,.rs-pult.st-modal .st-range::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:22px;border-radius:50%;background:#fff;border:2px solid rgba(0,0,0,.45);box-shadow:0 1px 5px rgba(0,0,0,.55);cursor:pointer} .rs-pult.st-modal .st-hsl input::-moz-range-thumb,.rs-pult.st-modal .st-range::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid rgba(0,0,0,.45);cursor:pointer} .rs-pult.st-modal .st-prev{height:8px;border-radius:3px;background:var(--th)} .rs-pult.st-modal .st-val{flex:none;min-width:40px;text-align:right;font-size:13px;opacity:.7} .rs-pult.st-modal .st-tiles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px} .rs-pult.st-modal .st-tile{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;min-height:62px;margin:0;padding:6px 2px;border-radius:12px;border:1px solid var(--pl);background:var(--pt);color:#fff;font:500 11px/1.15 Onest,system-ui,sans-serif;text-align:center;opacity:.5;cursor:pointer;transition:opacity .12s,transform .12s} .rs-pult.st-modal .st-tile.on{opacity:1;border-color:var(--th)} .rs-pult.st-modal .st-tile:active{transform:scale(.97)} .rs-pult.st-modal .st-tile .st-ic{filter:none;color:#fff} .rs-pult.st-modal .st-tile .st-tl{max-width:100%;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}`; class SettingsManager { constructor(ui) { this.ui = ui; this.hslOpen = false; this.tab = 'look'; this.build(); } build() { const sh = buildModalShell(this.ui.shadow, 'bm-modal rs-modal rs-pult st-modal'); this.overlay = sh.overlay; this.title = sh.title; this.body = sh.body; sh.modal.style.setProperty('background', 'var(--pb)', 'important'); sh.closeBtn.innerHTML = ICON_CLOSE; const css = document.createElement('style'); css.textContent = RS_PULT_CSS + ST_CSS; sh.overlay.appendChild(css); this.tabsEl = mk('div', 'st-tabs'); sh.modal.insertBefore(this.tabsEl, sh.body); this.overlay.onclick = e => { if (e.target === this.overlay && Date.now() - (this._opAt || 0) > 500) this.close(); }; sh.closeBtn.onclick = () => this.close(); } open() { this.render(); this._opAt = Date.now(); this.overlay.classList.add('open'); } close() { this.overlay.classList.remove('open'); } go(open, getOverlay) { const prev = this.overlay; noFade(prev); open(); const next = getOverlay(); noFade(next); prev.classList.remove('open'); stBack(this.ui, next); } updateLabels() { if (this.overlay.classList.contains('open')) this.render(); this.ui.hideManager?.overlay?._bk?.(); this.ui.recorder?._rs?.overlay?._bk?.(); this.ui.sleepTimer?.modal?.overlay?._bk?.(); this.ui.bookmarkManager?.overlay?._bk?.(); } setTheme(c) { AppState.theme = c; Store.set('bc_theme', c); this.ui.host.style.setProperty('--th', c); } randomTheme() { const r = (a, b) => Math.floor(a + Math.random() * (b - a)); this.setTheme(`hsla(${r(0, 360)},${r(35, 55)}%,${r(35, 50)}%,0.85)`); } hslPicker(l, swRow) { const [h0, s0, l0] = colorToHsl(AppState.theme); const st = { h: h0, s: s0, l: clamp(l0, 15, 85) }; const box = mk('div', 'st-hsl'); const prev = mk('div', 'st-prev'); const tracks = []; const slider = (label, key, max, min = 0) => { const wrap = mk('div', 'st-hrow'); const inp = mk('input'); inp.type = 'range'; inp.min = String(min); inp.max = String(max); inp.step = '1'; inp.value = String(st[key]); inp.oninput = () => { st[key] = +inp.value; this.setTheme(`hsla(${st.h},${st.s}%,${st.l}%,0.9)`); swRow.querySelectorAll('.st-swatch.sel').forEach(x => x.classList.remove('sel')); swRow.querySelector('.st-custom')?.classList.add('sel'); paint(); }; wrap.append(textEl('', label, 'span'), inp); tracks.push([key, inp]); return wrap; }; const paint = () => { tracks.forEach(([key, inp]) => { inp.style.background = key === 'h' ? 'linear-gradient(90deg,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)' : key === 's' ? `linear-gradient(90deg,hsl(${st.h},0%,${st.l}%),hsl(${st.h},100%,${st.l}%))` : `linear-gradient(90deg,hsl(${st.h},${st.s}%,15%),hsl(${st.h},${st.s}%,50%),hsl(${st.h},${st.s}%,85%))`; }); }; box.append(slider(l.st_hue, 'h', 360), slider(l.st_sat, 's', 100), slider(l.st_lig, 'l', 85, 15), prev); paint(); return box; } renderKit(root) { let cur = root; const frag = { append: (...n) => cur.append(...n) }; const ic = k => `<span class="st-ic">${icons[k] || ''}</span>`; const sec = (k, t) => { const grp = mk('div', 'rs-g'); grp.append(textEl('hd-sec', t)); root.append(grp); cur = grp; }; const lbl = (k, t) => { const e = mk('span', 'st-lbl'); e.innerHTML = `${ic(k)}<span>${escHTML(t)}</span>`; return e; }; const row = (k, label, ...ctl) => { const r = mk('div', 'st-row'); r.append(lbl(k, label), ...ctl); frag.append(r); }; const pill = (t, fn, k, extra = '') => { const b = mk('button', `st-pill${extra}`); b.innerHTML = `${k ? icons[k] : ''}<span>${escHTML(t)}</span>`; b.onclick = fn; return b; }; const sw = (on, fn) => { const b = mk('button', `bm-sw${on ? ' on' : ''}`); b.onclick = () => { on = !on; b.classList.toggle('on', on); fn(on); }; return b; }; return { frag, ic, sec, lbl, row, pill, sw }; } stThemeBlock(k, l) { const { frag, lbl, pill } = k; const colBox = mk('div', 'st-block'); const swRow = mk('div', 'st-sw-row'); const swatches = themeSwatches(); const known = swatches.includes(AppState.theme); swatches.forEach(c => { const s = mk('button', `st-swatch${AppState.theme === c ? ' sel' : ''}`); s.style.background = c; s.onclick = () => { this.setTheme(c); this.hslOpen = false; this.render(); }; swRow.append(s); }); const custom = mk('button', `st-swatch st-custom${!known ? ' sel' : ''}`); custom.title = l.st_custom; custom.onclick = () => { this.hslOpen = !this.hslOpen; this.render(); }; swRow.append(custom, pill(l.st_color_rnd, () => { this.randomTheme(); this.hslOpen = false; this.render(); }, 'dice')); colBox.append(lbl('color', l.st_color), swRow); if (this.hslOpen) colBox.append(this.hslPicker(l, swRow)); frag.append(colBox); } stLook(k, l, ui) { const { sec, row, pill, sw } = k; sec('color', l.st_look); row('globe', l.st_lang, pill(`${AppState.lang} · ${LANG_NAMES[AppState.lang] || ''}`, () => ui.openLangPicker())); this.stThemeBlock(k, l); const rng = mk('input', 'st-range'); rng.type = 'range'; rng.min = '60'; rng.max = '150'; rng.step = '10'; rng.value = String(Math.round(AppState.scale * 100)); const val = textEl('st-val', `${rng.value}%`, 'span'); rng.oninput = () => { val.textContent = `${rng.value}%`; ui.setScale(rng.value / 100); }; row('grid', l.st_scale, rng, val); row('moon', l.st_site_dark, sw(AppState.dark, v => ui.toggleDark(v))); row('eyeOff', l.hd_title, pill(l.st_open, () => this.go(() => ui.hideManager?.open(), () => ui.hideManager?.overlay))); } stButtons(k, l, ui) { const { frag, ic, sec, pill } = k; sec('grid', l.st_btns); frag.append(textEl('hd-hint', l.st_btns_hint)); const tiles = mk('div', 'st-tiles'); const hid = ui.hiddenButtons(); ui.panelItems().forEach(([id, el, key]) => { if (!el) return; const name = l[key] || key; const t = mk('button', `st-tile${hid[id] ? '' : ' on'}`); t.title = name; t.innerHTML = `${ic(TILE_ICONS[id])}<span class="st-tl">${escHTML(name)}</span>`; t.onclick = () => { ui.setButtonHidden(id, t.classList.contains('on')); t.classList.toggle('on'); }; tiles.append(t); }); frag.append(tiles, pill(l.st_btns_reset, () => { ui.resetButtons(); this.render(); }, 'rel', ' st-wide')); } stTools(k, l, ui) { const { sec, row, pill, sw } = k; sec('rec', l.st_rec); row('rec', l.rs_title, pill(l.st_open, () => this.go(() => ui.recorder?.openSettings(), () => ui.recorder?._rs?.overlay))); sec('grid', mvT('mv_title')); row('grid', mvT('mv_title'), pill(l.st_open, () => this.go(() => ui.multiView?.open(), () => ui.bookmarkManager?.overlay))); row('grid', mvT('mv_auto_all'), sw(Store.get('bc_mv_auto', true) !== false, v => ui.multiView?.setAuto(v))); row('grid', mvT('mv_bg'), sw(mvBg(), v => { Store.set('bc_mv_bg', v); if (ui.multiView?.pane) ui.multiView.labels(); })); sec('sleep', l.sleep_title); const slp = ui.sleepTimer; row('sleep', slp?.end ? `${l.sleep_left}: ${slp.fmt()}` : l.rs_off, pill(l.st_open, () => this.go(() => ui.sleepTimer?.open(), () => ui.sleepTimer?.modal?.overlay))); sec('target', l.rad_title); row('target', l.rad_title, sw(ui.radial?.on === true, v => ui.radial?.setEnabled(v))); row('rel', l.rad_reset, pill(l.cal_reset, () => ui.radial?.resetPos())); } stPreview(k) { const api = CBHP.api; if (SITE !== 'cb' || !api) return; const { sec, row, sw } = k; const ru = /^(RU|UK|BE)$/i.test(AppState.lang); const t = (a, b) => (ru ? a : b); const labels = { off: t('Выкл', 'Off'), anim: t('Обложка', 'Cover'), video: t('Видео', 'Video') }; const chips = (items, cur, pick) => { const box = mk('div', 'rs-chips'); items.forEach(([val, text]) => { const b = textEl(`bm-tbtn${val === cur ? ' on' : ''}`, text, 'button'); b.onclick = () => { pick(val); this.render(); }; box.append(b); }); return box; }; sec('eye', t('Превью при наведении', 'Hover preview')); row('eye', t('Режим', 'Mode'), chips(api.modes.map(m => [m, labels[m]]), api.cfg.mode, api.setMode)); row('vol', t('Звук', 'Sound'), sw(api.cfg.sound, v => api.setSound(v))); row('qual', t('Качество видео', 'Video quality'), chips(api.heights.map(h => [h, `${h}p`]), api.cfg.maxH, api.setMaxH)); } stFavHist(k, l, ui) { const { sec, row, pill, sw } = k; sec('fav', l.st_fav); row('star', l.st_star, sw(ui.thumbStars?.on !== false, v => ui.thumbStars?.setEnabled(v))); row('bookmark', l.st_list, pill(l.st_open, () => this.go(() => ui.bookmarkManager?.open(), () => ui.bookmarkManager?.overlay))); sec('history', l.st_hist); row('eye', l.st_hist_on, sw(histOn(), v => Store.set('bc_hist_on', v))); row('list', l.st_list, pill(l.st_open, () => this.go(() => ui.historyManager?.open(), () => ui.bookmarkManager?.overlay))); row('cal', l.cal_title, pill(l.st_open, () => this.go(() => ui.bookmarkManager?.open('cal'), () => ui.bookmarkManager?.overlay))); } stData(k, l, ui) { const { frag, sec, lbl, row, pill } = k; sec('dl', l.st_data); const bkBox = mk('div', 'st-block'); const bkChips = mk('div', 'rs-chips'); const bkParts = Backup.parts(); [['s', l.bk_p_set], ['f', l.bk_p_fav], ['h', l.bk_p_hist]].forEach(([key, t]) => { const b = textEl(`bm-tbtn${bkParts[key] ? ' on' : ''}`, t, 'button'); b.onclick = () => { if (Backup.setPart(key, !bkParts[key])) this.render(); }; bkChips.append(b); }); bkBox.append(lbl('dl', l.bk_parts), bkChips); frag.append(bkBox); row('dl', l.bk_exp, pill('JSON', () => Backup.export())); row('ul', l.bk_imp, pill('JSON', () => ui.backupImport())); const csvBox = mk('div', 'st-block'); const csvChips = mk('div', 'rs-chips'); csvChips.append( pill(l.bk_csv_fav, () => Backup.exportFavCsv()), pill(l.bk_csv_hist, () => ui.historyManager?.exportCsv(false)), pill(l.bk_csv_ses, () => ui.historyManager?.exportCsv(true)) ); csvBox.append(lbl('table', l.bk_csv), csvChips); frag.append(csvBox); const pst = ParserDiag.stats(); sec('list', pdT('diag')); row('list', `${pdT('title')}${pst.n ? ` (${pst.u}/${pst.n} ${pdT('nostatus')})` : ''}`, pill(l.st_open, () => ParserReport.open())); } render() { const l = L(), ui = this.ui; const keep = this._tabSw ? 0 : this.body.scrollTop; this._tabSw = false; this.title.innerHTML = `<span class="rs-ic">${icons.gear}</span><span>${escHTML(l.settings)}</span>`; const ru = /^(RU|UK|BE)$/i.test(AppState.lang); const tabs = [['look', ru ? 'Вид' : 'Look'], ['btns', ru ? 'Кнопки' : 'Buttons'], ['tools', ru ? 'Функции' : 'Tools'], ['data', ru ? 'Данные' : 'Data']]; if (!tabs.some(x => x[0] === this.tab)) this.tab = 'look'; this.tabsEl.replaceChildren(...tabs.map(([id, text]) => { const b = textEl(`st-tab${id === this.tab ? ' on' : ''}`, text, 'button'); b.type = 'button'; b.onclick = () => { if (this.tab === id) return; this.tab = id; this._tabSw = true; this.render(); }; return b; })); const frag = document.createDocumentFragment(); const k = this.renderKit(frag); if (this.tab === 'look') this.stLook(k, l, ui); else if (this.tab === 'btns') this.stButtons(k, l, ui); else if (this.tab === 'tools') { this.stTools(k, l, ui); this.stPreview(k); this.stFavHist(k, l, ui); } else this.stData(k, l, ui); this.body.replaceChildren(frag); this.body.scrollTop = keep; } } const clampBarX = x => `${clamp(parseFloat(x) || 0, 0, Math.max(0, window.innerWidth - 40))}px`; const clampBarB = v => `${clamp(Math.round(parseFloat(v) || 0), 0, Math.max(0, window.innerHeight - 40))}px`; class UIController { constructor() { this.elements = {}; this.hasMoved = false; this.buildShell(); this.buildHeader(); this.buildMediaRow(); this.buildVolume(); this.buildControls(); this.buildMisc(); this.buildSettings(); this.applyButtons(); this.watchLabelFit(); this.setupDrag(); this.setupResize(); this.applyPosition(); this.setupFilterDrag(); document.documentElement.appendChild(this.host); } buildShell() { this.host = document.createElement('div'); this.host.id = 'bc-unchained-host'; this.shadow = this.host.attachShadow({ mode: 'open' }); if (document.documentElement.classList.contains('bc-dark-fb')) this.host.setAttribute('data-dfb', ''); const style = document.createElement('style'); style.textContent = CSS_UI + CSS_X + CSS_NOSEL + CSS_POLISH + CSS_COMPACT + CSS_PULT; this.shadow.appendChild(style); this.panel = mk('div', 'panel'); this.panel.style.transform = `scale(${AppState.scale})`; this.shadow.appendChild(this.panel); this.ghostBtn = mk('div', 'ghost'); this.ghostBtn.innerHTML = icons.menu; this.ghostBtn.onclick = () => this.toggleHide(false); this.shadow.appendChild(this.ghostBtn); this.restoreBtn = mk('div', 'restore'); this.restoreBtn.innerHTML = icons.menu; this.restoreBtn.onclick = () => { if (!this.hasMoved) this.toggleMinimize(false); }; this.restoreBtn.onpointerdown = e => this.onDragStart(e); this.panel.appendChild(this.restoreBtn); this.fullContainer = mk('div', 'col'); this.panel.appendChild(this.fullContainer); } fitLabels() { this.panel.querySelectorAll('.btn sub').forEach(sub => { const btn = sub.closest('.btn'); if (!btn || !btn.clientWidth) return; const sig = `${btn.clientWidth}|${sub.textContent}`; if (sub._fitSig === sig) return; sub.style.removeProperty('font-size'); sub.style.setProperty('white-space', 'nowrap', 'important'); let fs = parseFloat(getComputedStyle(sub).fontSize) || 10.5; const room = btn.clientWidth - 4; while (sub.scrollWidth > room && fs > 6.5) { fs -= 0.5; sub.style.setProperty('font-size', `${fs}px`, 'important'); } sub._fitSig = sig; }); } watchLabelFit() { let raf = 0; const run = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => this.fitLabels()); }; new MutationObserver(run).observe(this.panel, { childList: true, subtree: true, characterData: true }); window.addEventListener('resize', run); run(); setTimeout(run, 400); } setupFilterDrag() { const fs = { hasMoved: false, dragTarget: null, iL: 0, iT: 0, cFx: Store.get('bc_fx', null), cFb: readFb() }; const applyPos = this.filterBarPositioner(fs); this.bindFilterBarDrag(fs, applyPos); } filterBarPositioner(fs) { const setImp = (el, props) => { const st = el.style; for (const k in props) { if (st.getPropertyValue(k) !== props[k] || st.getPropertyPriority(k) !== 'important') st.setProperty(k, props[k], 'important'); } }; const applyPos = () => { const b = document.querySelector('.bc_sticky_bar'); if (!b || fs.dragTarget || realHidden() || window.innerHeight < 200) return; if (!(fs.cFx && fs.cFb)) return setImp(b, { left: '50%', transform: 'translateX(-50%)' }); const x = clampBarX(fs.cFx), v = clampBarB(fs.cFb); setImp(b, { left: x, top: 'auto', bottom: v, right: 'auto', transform: 'none', margin: '0px', width: 'max-content' }); if (!b.hasAttribute('data-bcu')) b.setAttribute('data-bcu', '1'); if (!watched.has(b)) { watched.add(b); barObs.observe(b, { attributes: true, attributeFilter: ['style', 'class'] }); } }; const watched = new WeakSet(); let pend = false; const schedule = () => { if (pend) return; pend = true; requestAnimationFrame(() => { pend = false; applyPos(); }); }; const barObs = new MutationObserver(schedule); DomBus.subscribe(muts => { for (const m of muts) for (const n of m.addedNodes) { if (n.nodeType === 1 && (n.matches?.('.bc_sticky_bar') || n.querySelector?.('.bc_sticky_bar'))) return schedule(); } }); const css0 = document.getElementById('bc-unchained-site-css'); if (fs.cFx && fs.cFb && css0 && !css0.textContent.includes('data-bcu')) { css0.remove(); injectSiteCSS(); } setInterval(() => { if (!realHidden()) applyPos(); }, 5000); window.addEventListener('resize', applyPos, { passive: true }); applyPos(); return applyPos; } bindFilterBarDrag(fs, applyPos) { window.addEventListener('pointerdown', e => { if (!isPrimaryPress(e)) return; const handle = e.target.closest('.js-filter_button, .bc_sticky_bar'), bar = document.querySelector('.bc_sticky_bar'); if (!handle || !bar) return; if (e.pointerType !== 'mouse') e.preventDefault(); fs.dragTarget = bar; const r = bar.getBoundingClientRect(); fs.iL = r.left; fs.iT = r.top; bar.style.cssText += `;transform:none!important;margin:0!important;bottom:auto!important;right:auto!important;width:max-content!important;left:${fs.iL}px!important;top:${fs.iT}px!important;`; handle.style.setProperty('cursor', 'grabbing', 'important'); Helpers.trackDrag(e, { minDist: 4, onMove: (dx, dy) => { if (!fs.dragTarget.isConnected) return; fs.dragTarget.style.setProperty('left', `${fs.iL + dx}px`, 'important'); fs.dragTarget.style.setProperty('top', `${fs.iT + dy}px`, 'important'); }, onEnd: (moved) => { fs.hasMoved = moved; if (moved && fs.dragTarget?.isConnected) { const r = fs.dragTarget.getBoundingClientRect(); fs.cFx = clampBarX(r.left); fs.cFb = clampBarB(window.innerHeight - r.bottom); Store.set('bc_fx', fs.cFx); Store.set('bc_fb', fs.cFb); } handle.style.setProperty('cursor', 'grab', 'important'); fs.dragTarget = null; applyPos(); } }); }, { passive: false, capture: true }); window.addEventListener('click', e => { if (fs.hasMoved && e.target.closest('.js-filter_button, .bc_sticky_bar')) { swallow(e); } }, { capture: true }); } buildHeader() { const hdr = mk('div', 'hdr'); this.elements.dragZone = mk('div', 'drag'); let dClicks = 0, dTimer = null; hdr.addEventListener('pointerdown', e => { if (e.target.closest('button, .stats')) return; dClicks++; if (dClicks === 1) dTimer = setTimeout(() => dClicks = 0, 500); else if (dClicks === 3) { clearTimeout(dTimer); dClicks = 0; AppState.posX = '15px'; AppState.posY = '10%'; AppState.hasPos = false; Store.set('bc_pos', false); this.applyPosition(); } this.onDragStart(e); }); this.elements.stats = mk('div', 'stats'); this.elements.stats.innerHTML = `${icons.eye}<span>–</span>`; const makeHBtn = (html, cls, click) => { const b = mk('button', `h-btn ${cls}`.trim()); b.innerHTML = html; b.onclick = click; return b; }; this.elements.bLang = makeHBtn(icons.gear, 'lang', () => this.settingsManager?.open()); this.elements.bHome = makeHBtn(icons.home, '', () => location.href = '/'); this.elements.bRel = makeHBtn(icons.rel, '', () => location.reload()); this.elements.bMin = makeHBtn(icons.min, '', () => this.toggleMinimize(true)); this.elements.bHide = makeHBtn(icons.close, 'close', () => this.toggleHide(true)); const btns = mk('div', 'hdr-btns'); btns.append(this.elements.stats, mk('span', 'hsep'), this.elements.bLang, mk('span', 'hsep'), this.elements.bHome, this.elements.bRel, mk('span', 'hsep'), this.elements.bMin, this.elements.bHide); hdr.append(this.elements.dragZone, btns); this.fullContainer.appendChild(hdr); } buildMediaRow() { const row = mk('div', 'grid-4'); this.elements.bBg = Helpers.createButton(AppState.bgPlay ? icons.bgOn : icons.bgOff, ''); if (AppState.bgPlay) this.elements.bBg.classList.add('on'); this.elements.bBg.onclick = () => { AppState.bgPlay = !AppState.bgPlay; Store.set('bc_bg', AppState.bgPlay); this.elements.bBg.firstChild.innerHTML = AppState.bgPlay ? icons.bgOn : icons.bgOff; this.elements.bBg.querySelector('sub').textContent = L().bg; this.elements.bBg.classList.toggle('on', AppState.bgPlay); this.enforceBackgroundPlay(); }; this.elements.bMute = Helpers.createButton(icons.vol, ''); this.elements.bShot = Helpers.createButton(icons.snap, ''); this.elements.bRec = Helpers.createButton(icons.rec, ''); row.append(this.elements.bBg, this.elements.bMute, this.elements.bShot, this.elements.bRec); this.fullContainer.appendChild(row); } buildVolume() { const row = mk('div', 'v-cap'); this.elements.vIco = mk('span', 'vico'); this.elements.vIco.innerHTML = icons.vol; this.elements.vSlider = mk('input', 'vslider'); this.elements.vSlider.type = 'range'; this.elements.vSlider.min = '0'; this.elements.vSlider.max = '1'; this.elements.vSlider.step = '0.02'; this.elements.vSlider.value = '1'; this.elements.vVal = mk('span', 'vval'); this.elements.vVal.textContent = '100%'; row.append(this.elements.vIco, this.elements.vSlider, this.elements.vVal); this.fullContainer.appendChild(row); } buildControls() { this.fullContainer.appendChild(mk('div', 'sep')); const row = mk('div', 'grid-4'); this.elements.bQual = Helpers.createButton(icons.qual, ''); this.elements.bQual.onclick = () => this.toggleQuality(); this.elements.bPip = Helpers.createButton(icons.pip, ''); this.elements.bFs = Helpers.createButton(icons.fs, ''); this.elements.bThtr = Helpers.createButton(icons.thtr, ''); row.append(this.elements.bQual, this.elements.bPip, this.elements.bFs, this.elements.bThtr); this.fullContainer.appendChild(row); } bindQualityClose(qBlock) { if (qBlock._bcCloseBound) return; qBlock._bcCloseBound = true; qBlock.addEventListener('click', e => { if (!e.target.closest('.js-qsb_close, .qsb_close, .qsb_list_wrp, [class*="qsb"]')) return; setTimeout(() => { qBlock.classList.add('__hidden'); qBlock.style.display = 'none'; }, 50); }); } clickQualityFallback() { const q = document.querySelector('#quality-btn, .player-resolution, button.vjs-icon-hd, .vjs-resolution-button, .vjs-cog-menu-button, .vjs-icon-cog, [class*="quality"], [class*="resolution"]'); return q ? q.click() : this.flash(this.elements.bQual, `! ${L().err}`); } cbQualityMenu() { const old = document.getElementById('bc-cb-qual'); if (old) { old.remove(); return true; } const btn = document.querySelector('button.vjs-icon-hd'); const items = btn?.parentElement ? [...btn.parentElement.querySelectorAll('.vjs-menu-item')] : []; if (!items.length) return false; const th = AppState.theme; const mixOk = typeof CSS !== 'undefined' && CSS.supports?.('color', 'color-mix(in srgb, red 10%, blue)'); const mix = (pct, base) => (mixOk ? `color-mix(in srgb, ${th} ${pct}%, ${base})` : base); const pb = mix(7, '#0b0c0f'), pc = mix(11, '#15171b'), pl = mix(38, '#3a3e46'), pt = mix(14, '#22252b'); const font = 'Onest,system-ui,-apple-system,sans-serif'; const el = (css, tag = 'div') => { const e = document.createElement(tag); e.style.cssText = css; return e; }; const wrap = el('position:fixed;inset:0;z-index:2147483646;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45)'); wrap.id = 'bc-cb-qual'; const card = el(`width:min(86vw,320px);max-height:84vh;display:flex;flex-direction:column;overflow:hidden;border-radius:18px;background:${pb};border:1px solid ${pl};box-shadow:0 14px 36px rgba(0,0,0,.6);color:#fff;font-family:${font};box-sizing:border-box`); const body = el('flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px;box-sizing:border-box'); const grp = el('display:flex;flex-direction:column;gap:6px;padding:8px;border-radius:14px;background:rgba(0,0,0,.34)'); items.forEach(li => { const on = li.classList.contains('vjs-selected') || li.getAttribute('aria-checked') === 'true'; const b = el(`min-height:52px;padding:0 16px;display:flex;align-items:center;border-radius:12px;border:1px solid ${on ? 'transparent' : pl};background:${on ? th : pt};color:#fff;font:500 16px/1.2 ${font};text-align:left;cursor:pointer;box-sizing:border-box`, 'button'); b.type = 'button'; b.textContent = (li.querySelector('.vjs-menu-item-text') || li).textContent.trim(); b.onclick = e => { e.stopPropagation(); li.click(); wrap.remove(); }; grp.appendChild(b); }); body.appendChild(grp); const foot = el(`flex:none;display:flex;align-items:center;gap:8px;padding:8px;background:${pc};border-top:1px solid ${pl};box-sizing:border-box`); const ic = el(`flex:none;width:40px;height:40px;display:flex;align-items:center;justify-content:center;border-radius:12px;background:${th};color:#fff`); ic.innerHTML = icons.qual; ic.querySelector('svg')?.setAttribute('width', '22'); ic.querySelector('svg')?.setAttribute('height', '22'); const ttl = el('flex:1 1 auto;min-width:0;font-size:15px;font-weight:500', 'span'); ttl.textContent = ({ RU: 'Качество', UK: 'Якість' })[AppState.lang] || 'Quality'; const x = el(`flex:none;width:44px;height:44px;margin:0;padding:0;display:flex;align-items:center;justify-content:center;border-radius:12px;background:${pb};border:1px solid ${pl};color:#fff;cursor:pointer`, 'button'); x.type = 'button'; x.innerHTML = ICON_CLOSE; x.onclick = e => { e.stopPropagation(); wrap.remove(); }; foot.append(ic, ttl, x); card.append(body, foot); wrap.onclick = e => { if (e.target === wrap) wrap.remove(); }; wrap.appendChild(card); if (document.documentElement.classList.contains('bc-dark-fb')) wrap.style.filter = 'invert(1) hue-rotate(180deg)'; document.documentElement.appendChild(wrap); return true; } toggleQuality() { const qBlock = document.querySelector('#quality_settings_block, .js-quality_settings_block'); if (!qBlock) return this.cbQualityMenu() || this.clickQualityFallback(); this.bindQualityClose(qBlock); const vis = isQualityShown(qBlock); qBlock.classList.toggle('__hidden', vis); qBlock.style.display = vis ? 'none' : 'block'; if (vis) return; qBlock.style.zIndex = '2147483645'; try { jq()?.('#quality-btn').trigger('click'); } catch (e) { dbg(e, 'UIController.toggleQuality'); } } buildMisc() { const row = mk('div', 'grid-4'); this.elements.bSearch = Helpers.createButton(icons.search, ''); this.elements.bHls = Helpers.createButton(icons.hls, 'HLS'); const hlsFlash = ok => this.flash(this.elements.bHls, ok ? 'OK' : 'ERR'); Helpers.bindLongPress(this.elements.bHls, () => copyHls(getStreamUrl(), hlsFlash), () => shareHls(getStreamUrl(), `${currentSite.getName()} Stream`, hlsFlash) ); this.elements.bCopy = Helpers.createButton(icons.copy, ''); this.elements.bCopy.onclick = () => { const name = currentSite.getName(); if (!name) return this.flash(this.elements.bCopy, 'ERR'); const link = modelSlug(location.href) ? linkFor(location.href) : `${location.origin}${location.pathname}`; Helpers.copy(link, () => this.flash(this.elements.bCopy, 'OK'), () => this.flash(this.elements.bCopy, 'ERR')); }; this.elements.bFav = Helpers.createButton(icons.fav, ''); row.append(this.elements.bSearch, this.elements.bHls, this.elements.bCopy, this.elements.bFav); this.fullContainer.appendChild(row); const navRow = mk('div', 'favnav'); this.elements.bFavPrev = Helpers.createButton(icons.favPrev, ''); this.elements.bFavNext = Helpers.createButton(icons.favNext, ''); navRow.append(this.elements.bFavPrev, this.elements.bFavNext); this.fullContainer.appendChild(navRow); this.elements.favNavRow = navRow; FavNav.hook = on => navRow.classList.toggle('on', on); FavNav.apply(); } buildSettings() { this.fullContainer.appendChild(mk('div', 'sep')); this.buildLookRow(); this.buildActionRow(); } buildScaleCtrl() { const scaleCtrl = mk('div', 'scale-ctrl'); const btnZoomOut = mk('button', 'z-btn'); btnZoomOut.innerHTML = '−'; const btnZoomIn = mk('button', 'z-btn'); btnZoomIn.innerHTML = '+'; const scaleVal = mk('span', 's-val'); scaleVal.textContent = `${Math.round(AppState.scale * 100)}%`; const setScale = s => { s = clamp(s, 0.6, 1.5); AppState.scale = s; Store.set('bc_scale', s); scaleVal.textContent = `${Math.round(s * 100)}%`; this.panel.style.transform = `scale(${s})`; if (AppState.hasPos) this.clampPosition(); }; this.setScale = setScale; this.elements.scaleCtrl = scaleCtrl; btnZoomOut.onclick = () => setScale(AppState.scale - 0.1); btnZoomIn.onclick = () => setScale(AppState.scale + 0.1); scaleCtrl.append(btnZoomOut, scaleVal, btnZoomIn); return scaleCtrl; } buildLookRow() { const row = mk('div', 'row-4'); const btnCol = mk('button', 'z-btn'); btnCol.innerHTML = icons.color; btnCol.style.cssText = 'width:100%;height:24px;'; this.elements.bColor = btnCol; const rnd = (min, max) => Math.floor(min + Math.random() * (max - min)); btnCol.onclick = () => { const c = `hsla(${rnd(0, 360)},${rnd(35, 55)}%,${rnd(35, 50)}%,0.85)`; AppState.theme = c; Store.set('bc_theme', c); this.host.style.setProperty('--th', c); }; const scaleCtrl = this.buildScaleCtrl(); const btnHide = mk('button', 'z-btn'); btnHide.innerHTML = icons.eyeOff; btnHide.style.cssText = 'width:100%;height:24px;'; btnHide.onclick = () => this.hideManager?.open(); this.elements.bHideMgr = btnHide; row.style.cssText = 'display:grid;grid-template-columns:repeat(4,1fr);gap:6px;align-items:center'; scaleCtrl.style.cssText = 'grid-column:span 2;justify-content:space-between'; row.append(btnCol, btnHide, scaleCtrl); this.fullContainer.appendChild(row); } buildActionRow() { const row2 = mk('div', 'row-2'); row2.style.cssText = 'display:flex;gap:6px'; const sBtn = (key, fn) => { const b = mk('button', 'z-btn'); b.style.cssText = 'flex:1;height:24px;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;white-space:nowrap'; b.onclick = fn; this.elements[key] = b; return b; }; row2.append( sBtn('bDark', () => this.toggleDark()), sBtn('bBkEx', () => { Backup.export(); this.flash(this.elements.bBkEx, 'OK'); }), sBtn('bBkIm', () => this.backupImport()), sBtn('bDon', () => this.openDonate()) ); this.fullContainer.appendChild(row2); this.elements.hints = mk('div', 'hotkeys'); this.fullContainer.appendChild(this.elements.hints); } panelItems() { const e = this.elements; return [ ['bBg', e.bBg, 'bg'], ['bMute', e.bMute, 'sound'], ['bShot', e.bShot, 'snap'], ['bRec', e.bRec, 'rec'], ['vol', e.vIco?.parentElement, 'bt_vol'], ['bQual', e.bQual, 'qual'], ['bPip', e.bPip, 'pip'], ['bFs', e.bFs, 'fs'], ['bThtr', e.bThtr, 'thtr'], ['bSearch', e.bSearch, 'search'], ['bHls', e.bHls, 'HLS'], ['bCopy', e.bCopy, 'copy'], ['bFav', e.bFav, 'favs'], ['stats', e.stats, 'bt_stats'], ['bHome', e.bHome, 'home'], ['bRel', e.bRel, 'rel'], ['bColor', e.bColor, 'st_color'], ['bHideMgr', e.bHideMgr, 'hd_title'], ['scale', e.scaleCtrl, 'st_scale'], ['bDark', e.bDark, 'st_site_dark'], ['bBkEx', e.bBkEx, 'bk_exp'], ['bBkIm', e.bBkIm, 'bk_imp'], ['bDon', e.bDon, 'don_tip'], ['hints', e.hints, 'bt_hints'] ]; } hiddenButtons() { const o = Store.get('bc_btnhide', {}); return o && typeof o === 'object' && !Array.isArray(o) ? o : {}; } setButtonHidden(id, hide) { const o = { ...this.hiddenButtons() }; if (hide) o[id] = true; else delete o[id]; Store.set('bc_btnhide', o); this.applyButtons(); } resetButtons() { Store.set('bc_btnhide', {}); this.applyButtons(); } applyButtons() { const hid = this.hiddenButtons(); this.panelItems().forEach(([id, el]) => { if (el) el.classList.toggle('off', hid[id] === true); }); [...this.fullContainer.children].filter(c => c.matches('.grid-4,.row-4,.row-2')).forEach(r => { const n = [...r.children].filter(c => !c.classList.contains('off')).length; r.classList.toggle('off', n === 0); if (r.matches('.grid-4')) r.style.gridTemplateColumns = n > 0 && n < 4 ? `repeat(${n},minmax(0,1fr))` : ''; }); } openLangPicker() { if (!this._lp) { const { overlay, modal, title, closeBtn, body } = buildModalShell(this.shadow, 'bm-modal rs-modal rs-pult'); modal.style.setProperty('background', 'var(--pb)', 'important'); closeBtn.innerHTML = ICON_CLOSE; const css = document.createElement('style'); css.textContent = RS_PULT_CSS + SRCH_CSS + LP_CSS; overlay.appendChild(css); closeBtn.onclick = () => overlay.classList.remove('open'); overlay.onclick = e => { if (e.target === overlay && Date.now() - (this._lpAt || 0) > 400) overlay.classList.remove('open'); }; const box = mk('div', 'sr-box'); box.innerHTML = icons.search; const input = mk('input', 'sr-in'); input.type = 'text'; input.autocomplete = 'off'; const clear = mk('button', 'sr-x'); clear.type = 'button'; clear.innerHTML = SRCH_CLEAR; box.append(input, clear); const gSearch = mk('div', 'rs-g'); gSearch.append(box); const gAuto = mk('div', 'rs-g'); const gLang = mk('div', 'rs-g'); body.replaceChildren(gSearch, gAuto, gLang); this._lp = { overlay, title, input, clear, gAuto, gLang }; } const { overlay, title, input, clear, gAuto, gLang } = this._lp; const l = L(); const ru = /^(RU|UK|BE)$/i.test(AppState.lang); title.innerHTML = `<span class="rs-ic">${icons.globe}</span><span>${escHTML(l.lang_title)}</span>`; input.placeholder = l.lp_find || (ru ? 'Найти язык' : 'Find language'); input.value = ''; const choose = (code, auto) => { AppState.langAuto = auto; Store.set('bc_lang_auto', auto); AppState.lang = auto ? detectLang() : code; if (!auto) Store.set('bc_lang', code); this.updateLabels(); overlay.classList.remove('open'); }; const item = (code, text, on, fn, extra) => { const b = mk('button', `lp-btn${on ? ' sel' : ''}${extra ? ' ' + extra : ''}`); b.type = 'button'; b.innerHTML = `<span class="lp-code">${code}</span><span class="lp-name">${escHTML(text)}</span>${extra === 'lp-auto' && on ? `<span class="lp-ck">${LP_CHECK}</span>` : ''}`; b.onclick = fn; b._q = `${code} ${text}`.toLowerCase(); return b; }; const dl = detectLang(); const autoBtn = item(dl, `${l.lang_auto} · ${LANG_NAMES[dl]}`, AppState.langAuto, () => choose('', true), 'lp-auto'); const btns = LANGS.map(c => item(c, LANG_NAMES[c], !AppState.langAuto && AppState.lang === c, () => choose(c, false))); const grid = mk('div', 'lp-grid'); grid.append(...btns); const cnt = mk('span'); const hs = textEl('hd-sec lp-hs', ''); hs.append(textEl('', l.lang_title, 'span'), cnt); const empty = textEl('lp-empty', ru ? 'Ничего не найдено' : 'No matches'); gAuto.replaceChildren(textEl('hd-sec', ru ? 'Автоопределение' : 'Auto-detect'), autoBtn); gLang.replaceChildren(hs, grid, empty); const apply = () => { const q = input.value.trim().toLowerCase(); let n = 0; btns.forEach(b => { const ok = !q || b._q.includes(q); b.hidden = !ok; if (ok) n++; }); const aOk = !q || autoBtn._q.includes(q); autoBtn.hidden = !aOk; gAuto.hidden = !aOk; cnt.textContent = n; empty.hidden = n > 0 || aOk; gLang.hidden = n === 0 && aOk; }; input.oninput = apply; input.onkeydown = e => { e.stopPropagation(); if (e.key === 'Enter') { const f = btns.find(b => !b.hidden); if (f) f.click(); } }; clear.onclick = () => { input.value = ''; apply(); input.focus(); }; apply(); this._lpAt = Date.now(); overlay.classList.add('open'); } updateLabels() { const l = L(); this.host.toggleAttribute('data-rtl', RTL_LANGS.includes(AppState.lang)); this.elements.dragZone.textContent = ''; this.elements.bLang.title = l.settings; this.elements.bHome.title = l.home; this.elements.bRel.title = l.rel; this.elements.bMin.title = l.min; this.elements.bHide.title = l.hide; const subs = [ ['bBg', 'bg'], ['bMute', 'sound'], ['bShot', 'snap'], ['bRec', 'rec'], ['bQual', 'qual'], ['bPip', 'pip'], ['bFs', 'fs'], ['bThtr', 'thtr'], ['bSearch', 'search'], ['bCopy', 'copy'], ['bFav', 'favs'] ]; subs.forEach(([k, lk]) => { const s = this.elements[k]?.querySelector('sub'); if (s) s.textContent = l[lk]; }); this.elements.hints.innerHTML = l.hints; if (this.elements.bRec) this.elements.bRec.title = l.rec_tip; const e = this.elements; if (e.bDark) { e.bDark.innerHTML = (AppState.dark ? icons.moon : icons.sun); e.bDark.title = l.dark; e.bDark.style.background = AppState.dark ? 'linear-gradient(145deg,var(--th),rgba(255,255,255,.14))' : ''; e.bDark.style.boxShadow = AppState.dark ? 'inset 0 0 0 1px rgba(255,255,255,.22)' : ''; } [['bBkEx', 'dl', 'bk_exp'], ['bBkIm', 'ul', 'bk_imp'], ['bDon', 'coffee', 'don_tip']].forEach(([k, ico, tk]) => { if (e[k]) { e[k].innerHTML = icons[ico]; e[k].title = l[tk]; } }); if (e.bDon) e.bDon.style.color = '#ffb347'; this.bookmarkManager?.updateLabels(); this.hideManager?.updateLabels(); this.settingsManager?.updateLabels(); this.historyManager?.updateLabels(); } toggleDark(on = !AppState.dark) { AppState.dark = !!on; Store.set('bc_dark', AppState.dark); applyDark(); this.updateLabels(); } backupImport() { Backup.import( () => { this.flash(this.elements.bBkIm, 'OK'); setTimeout(() => location.reload(), 700); }, () => this.flash(this.elements.bBkIm, 'ERR') ); } donateWallets() { return [ { name: 'TON', net: 'Gram · USDT', sym: 'T', svg: '<svg viewBox="12 14 32 32" width="26" height="26" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><path fill="#fff" d="M37.56 15.63H18.44c-3.52 0-5.75 3.79-3.98 6.86l11.8 20.45c.77 1.34 2.7 1.34 3.47 0l11.8-20.45c1.77-3.06-.46-6.86-3.97-6.86zM26.25 36.81l-2.57-4.98-6.2-11.09c-.41-.71.1-1.62.95-1.62h7.82v17.69zm12.26-16.07-6.2 11.1-2.57 4.97V19.12h7.82c.86 0 1.36.91.95 1.62z"/></svg>', c1: '#2aa6f0', c2: '#0b6fc2', addr: 'UQAhsfD61tpWIWI_VUnvJzDXE5LkfZQd1uZoWFO0uDEY81Nq' }, { name: 'Litecoin', net: 'LTC', sym: 'Ł', svg: '<svg viewBox="0 0 24 24" width="26" height="26" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><path d="M10.8 4.8 8.6 16.4h7.4" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M6.4 12.9l8.2-3.3" fill="none" stroke="#fff" stroke-width="2.1" stroke-linecap="round"/></svg>', c1: '#9aa7b8', c2: '#4b5a70', addr: 'ltc1q7kdhehwusk6qupe325trqe9xjhp6qq4rpgy88j' }, { name: 'BNB Smart Chain', net: 'BEP-20 · USDT · BNB', sym: 'B', svg: '<svg viewBox="0 0 24 24" width="26" height="26" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><g fill="#fff"><path d="M12 3 7.5 7.5l1.7 1.7L12 6.4l2.8 2.8 1.7-1.7zM12 21l-4.5-4.5 1.7-1.7 2.8 2.8 2.8-2.8 1.7 1.7z"/><path d="M5.2 9.9 7.3 12l-2.1 2.1L3.1 12zM18.8 9.9 20.9 12l-2.1 2.1-2.1-2.1zM12 9.2 14.8 12 12 14.8 9.2 12z"/></g></svg>', c1: '#f8cf4d', c2: '#c9930a', addr: '0xC1602C8CE455A4f7E694DF8524f59317de375310' }, { name: 'Ethereum', net: 'ERC-20 · ETH', sym: 'Ξ', svg: '<svg viewBox="0 0 256 417" width="17" height="28" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><path fill="#fff" fill-opacity=".6" d="M127.96 0v154.16l128 56.16z"/><path fill="#fff" d="M127.96 0 0 210.32l127.96-56.16z"/><path fill="#fff" fill-opacity=".6" d="M127.96 312.19V417L256 236.59z"/><path fill="#fff" d="M127.96 417V312.19L0 236.59z"/><path fill="#fff" fill-opacity=".2" d="m127.96 287.96 128-75.64-128-58.16z"/><path fill="#fff" fill-opacity=".6" d="m0 212.32 127.96 75.64V154.16z"/></svg>', c1: '#8a9bff', c2: '#4a5bd0', addr: '0xC1602C8CE455A4f7E694DF8524f59317de375310' }, { name: 'Solana', net: 'SOL · USDT', sym: 'S', svg: '<svg viewBox="0 0 398 312" width="24" height="19" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><g fill="#fff"><path d="M64.6 237.9c2.4-2.4 5.7-3.8 9.2-3.8h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1l62.7-62.7z"/><path d="M64.6 3.8C67.1 1.4 70.4 0 73.8 0h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1L64.6 3.8z"/><path d="M333.1 120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8 0-8.7 7-4.6 11.1l62.7 62.7c2.4 2.4 5.7 3.8 9.2 3.8h317.4c5.8 0 8.7-7 4.6-11.1l-62.7-62.7z"/></g></svg>', c1: '#9945ff', c2: '#14c9a0', addr: '7gp6seGi5aVJiKbMKxztPX8NBbtFsySRPfwmWmDEFM6P' } ]; } donateStyle() { const css = document.createElement('style'); css.textContent = `@keyframes dnPop{0%{transform:translateY(10px) scale(.98);opacity:0}100%{transform:none;opacity:1}} @keyframes dnSteam{0%{transform:translateY(5px) scaleX(.8);opacity:0}25%{opacity:.6}60%{transform:translateY(-5px) scaleX(1.15);opacity:.35}100%{transform:translateY(-13px) scaleX(.9);opacity:0}} @keyframes dnBeat{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}} .dn-modal{--pb:#0e0f12;--pc:#17191d;--pl:#2a2d33;--pt:#22252b;--pon:#fff;display:flex!important;flex-direction:column;font-family:Onest,system-ui,-apple-system,sans-serif;color:#fff;background:var(--pb)!important;border:1px solid var(--pl)!important;border-radius:18px!important;box-shadow:0 14px 36px rgba(0,0,0,.6)!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;padding:0;overflow:hidden} @supports(color:color-mix(in srgb,red 10%,blue)){.dn-modal{--pb:color-mix(in srgb,var(--th) 7%,#0b0c0f);--pc:color-mix(in srgb,var(--th) 11%,#15171b);--pl:color-mix(in srgb,var(--th) 38%,#3a3e46);--pt:color-mix(in srgb,var(--th) 14%,#22252b)}} @supports(color:oklch(from red l c h)){.dn-modal{--pon:oklch(from var(--th) calc((0.7 - l) * 1000) 0 0 / 1)}} .dn-modal *{box-sizing:border-box} .dn-grp{background:rgba(0,0,0,.34);border-radius:14px;padding:4px;display:flex;flex-direction:column;gap:6px} .dn-hdr{display:flex;align-items:center;gap:10px;padding:4px} .dn-cup{position:relative;flex:0 0 48px;width:48px;height:48px;border-radius:12px;background:var(--th);color:var(--pon);display:flex;align-items:center;justify-content:center} .dn-cup svg{display:block;overflow:visible} .dn-cup .dn-s{animation:dnWisp 3s ease-in-out infinite} .dn-cup .dn-s2{animation-delay:1.1s} @keyframes dnWisp{0%,100%{opacity:1;transform:translateY(0)}50%{opacity:.35;transform:translateY(-1.5px)}} .dn-steam{position:absolute;left:0;right:0;top:9px;height:0;pointer-events:none} .dn-steam i{position:absolute;bottom:100%;width:2px;height:11px;border-radius:1px;background:linear-gradient(rgba(255,255,255,0),rgba(255,255,255,.7) 55%,rgba(255,255,255,0));animation:dnSteam 3.4s cubic-bezier(.45,0,.55,1) infinite} .dn-top{display:flex;flex-direction:column;gap:2px} .dn-xrow{display:flex;justify-content:flex-end} .dn-hero{display:flex;flex-direction:column;align-items:center;text-align:center;gap:3px;padding:0 8px 6px;margin-top:-6px} .dn-hero .dn-cup{flex:none;width:64px;height:64px;border-radius:18px;margin-bottom:8px} .dn-hero .dn-t1{font-size:17px}.dn-hero .dn-t2{font-size:12px} .dn-heart{position:absolute;right:-6px;top:-6px;width:20px;height:20px;pointer-events:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5));animation:dnBeat 1.8s ease-in-out infinite} .dn-ttl{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px} .dn-t1{font-size:15px;font-weight:700;line-height:1.2;color:#fff} .dn-t2{font-size:11.5px;font-weight:500;line-height:1.3;color:#fff;opacity:.72} .dn-x{flex:0 0 52px;width:52px;height:52px;border-radius:14px;border:1px solid var(--pl);background:var(--pt);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;font:inherit;transition:background .12s,border-color .12s,transform .12s;-webkit-tap-highlight-color:transparent} .dn-x:hover{background:rgba(231,76,60,.9);border-color:transparent} .dn-x:active{transform:scale(.95)} .dn-lbl{display:flex;align-items:center;gap:8px;padding:6px 6px 0;font-size:10.5px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:#fff;opacity:.62} .dn-lbl::after{content:"";flex:1;height:1px;background:var(--pl);opacity:.7} .dn-row{position:relative;display:flex;align-items:center;gap:10px;padding:10px;min-height:64px;border-radius:14px;background:var(--pc);border:1px solid var(--pl);transition:background .15s,border-color .15s,transform .12s;-webkit-tap-highlight-color:transparent} .dn-row:active{transform:scale(.985)} .dn-list{background:var(--pc);border:1px solid var(--pl);border-radius:14px;overflow:hidden} .dn-list .dn-row{border:0;border-radius:0;background:transparent;min-height:60px} .dn-list .dn-row+.dn-row{border-top:1px solid var(--pl)} .dn-list .dn-ic{flex-basis:38px;width:38px;height:38px;border-radius:50%} .dn-list .dn-row:hover:not(.dn-ok){background:rgba(255,255,255,.04)} .dn-cta{display:flex;align-items:center;gap:10px;padding:10px;border-radius:14px;background:var(--th);color:var(--pon);cursor:pointer;transition:filter .12s,transform .12s;-webkit-tap-highlight-color:transparent} .dn-cta:hover{filter:brightness(1.08)}.dn-cta:active{transform:scale(.985)} .dn-cta .dn-ic{border-radius:10px;flex-basis:38px;width:38px;height:38px} .dn-cta .dn-mid{gap:1px} .dn-cta .dn-nm{color:var(--pon);font-size:14px} .dn-cta .dn-ad{font:500 12px/1.3 Onest,system-ui,sans-serif;color:var(--pon);opacity:.9;filter:none;cursor:pointer} .dn-cta>svg,.dn-cta>i{flex:none;display:block} .dn-row.dn-ok{background:rgba(61,220,132,.12)} .dn-ic{flex:0 0 40px;width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:17px;box-shadow:inset 0 1px rgba(255,255,255,.18)} .dn-mid{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px} .dn-nm{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;font-size:13.5px;font-weight:700;line-height:1.2;color:#fff} .dn-nt{font-size:10px;font-weight:600;color:#fff;opacity:.55;letter-spacing:.2px} .dn-ad{font:12px/1.35 ui-monospace,Menlo,Consolas,monospace;color:#fff;opacity:.82;cursor:pointer;word-break:break-all;-webkit-tap-highlight-color:transparent} .dn-ad.dn-sub{font:500 12px/1.35 Onest,system-ui,sans-serif;word-break:normal;color:var(--th);opacity:1;filter:brightness(1.35)} .dn-cp{flex:0 0 44px;width:44px;height:44px;border-radius:12px;border:1px solid var(--pl);background:var(--pt);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background .12s,border-color .12s,transform .12s;-webkit-tap-highlight-color:transparent} .dn-cp:hover{background:color-mix(in srgb,var(--pt) 78%,#fff)} .dn-cp:active{transform:scale(.94)} .dn-cp svg{display:block;margin:0!important} .dn-cp.on{background:var(--th);color:var(--pon);border-color:transparent} .dn-cp.go{background:var(--th);color:var(--pon);border-color:transparent} .dn-note{padding:6px 10px 4px;font-size:11px;line-height:1.5;color:#fff;opacity:.62;text-align:center} .dn-hint{padding:4px 8px 6px;font-size:11px;font-weight:500;color:#fff;opacity:.62;text-align:center} .dn-x:focus-visible,.dn-cp:focus-visible,.dn-row:focus-visible{outline:2px solid var(--th);outline-offset:2px} @media (max-width:520px){.bm-overlay:has(.dn-modal){align-items:stretch}.bm-modal.dn-modal{height:100%!important;max-height:100%!important;width:100vw!important;max-width:100vw!important;border-radius:0!important;border-left:0!important;border-right:0!important;border-bottom:0!important;align-self:stretch!important}} .dn-body{order:1;flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:8px;scrollbar-width:thin;scrollbar-color:var(--pl) transparent} .dn-body>*{flex-shrink:0} .dn-bar{order:2;flex:none;display:flex;align-items:center;gap:8px;padding:8px 8px calc(8px + env(safe-area-inset-bottom,0px));background:var(--pc);border-top:1px solid var(--pl)} .dn-bar .dn-cup{flex:0 0 44px;width:44px;height:44px;border-radius:12px} .dn-bar .dn-ttl{padding:0 2px} .dn-bar .dn-x{flex:0 0 44px;width:44px;height:44px;border-radius:12px;background:var(--pb)} .dn-bar .dn-heart{right:-6px;top:-6px;width:18px;height:18px}`; return css; } donateHead(tx) { const bar = mk('div', 'dn-bar'); const cup = mk('div', 'dn-cup'); cup.innerHTML = '<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path class="dn-s" d="M8 3a2.4 2.4 0 0 0-1 2 2.4 2.4 0 0 0 1 2"/><path class="dn-s dn-s2" d="M12 3a2.4 2.4 0 0 0-1 2 2.4 2.4 0 0 0 1 2"/><path d="M3 10h14v5a6 6 0 0 1-6 6H9a6 6 0 0 1-6-6v-5z"/><path d="M16.75 16.73a3 3 0 1 0 .25-5.56"/></svg>'; const heart = mk('div', 'dn-heart'); heart.innerHTML = '<svg viewBox="0 0 24 24" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"><path d="M12 21s-8-5.2-8-11a4.6 4.6 0 0 1 8-3.1A4.6 4.6 0 0 1 20 10c0 5.8-8 11-8 11z" fill="#ff5d73"/><path d="M7.5 8.5c.4-1.2 1.4-1.8 2.5-1.7" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" opacity=".6"/></svg>'; cup.appendChild(heart); const ttl = mk('div', 'dn-ttl'); const t1 = mk('div', 'dn-t1'); t1.textContent = tx.title; const t2 = mk('div', 'dn-t2'); t2.textContent = tx.sub; ttl.append(t1, t2); const x = mk('button', 'dn-x'); x.innerHTML = ICON_CLOSE; bar.append(cup, ttl, x); return { el: bar, closeBtn: x }; } donateRow(w, tx, toast) { const icCopy = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'; const icOk = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'; const short = a => a.length > 20 ? `${a.slice(0, 8)}…${a.slice(-8)}` : a; const row = mk('div', 'dn-row'); row.style.setProperty('--c', w.c1); const badge = mk('div', 'dn-ic'); badge.style.background = `linear-gradient(145deg,${w.c1},${w.c2})`; if (w.svg) badge.innerHTML = w.svg; else badge.textContent = w.sym; const mid = mk('div', 'dn-mid'); const nm = mk('div', 'dn-nm'); nm.textContent = w.name; if (w.net) { const nt = mk('span', 'dn-nt'); nt.textContent = w.net; nm.appendChild(nt); } const ad = mk('div', 'dn-ad'); let full = false; ad.textContent = short(w.addr); ad.onclick = () => { full = !full; ad.textContent = full ? w.addr : short(w.addr); }; mid.append(nm, ad); const cp = mk('button', 'dn-cp'); cp.innerHTML = icCopy; cp.onclick = () => Helpers.copy(w.addr, () => { cp.innerHTML = icOk; cp.classList.add('on'); row.classList.add('dn-ok'); toast(tx.copied, true); setTimeout(() => { if (cp.isConnected) { cp.innerHTML = icCopy; cp.classList.remove('on'); row.classList.remove('dn-ok'); } }, 1400); }, () => toast(tx.fail, false)); row.append(badge, mid, cp); return row; } donateToast() { const toastEl = mk('div'); toastEl.style.cssText = 'position:sticky;bottom:0;height:0;display:flex;justify-content:center;overflow:visible;z-index:3'; const toastIn = mk('div'); toastIn.style.cssText = 'position:absolute;bottom:14px;padding:9px 16px;border-radius:12px;font-size:12.5px;font-weight:600;color:#fff;background:#1f9d5c;border:1px solid rgba(255,255,255,.18);box-shadow:0 6px 18px rgba(0,0,0,.5);opacity:0;transform:translateY(8px);transition:.25s;pointer-events:none;white-space:nowrap'; toastEl.appendChild(toastIn); let tt = 0; const toast = (msg, ok) => { toastIn.textContent = msg; toastIn.style.background = ok ? '#1f9d5c' : '#c0392b'; toastIn.style.opacity = '1'; toastIn.style.transform = 'none'; clearTimeout(tt); tt = setTimeout(() => { toastIn.style.opacity = '0'; toastIn.style.transform = 'translateY(8px)'; }, 1500); }; return { el: toastEl, toast }; } openDonate() { const l = L(); const tx = { title: l.don_title, sub: l.don_sub, copied: l.don_copied, fail: l.don_fail, tap: l.don_tap }; if (this._donate && this._donate.lang !== AppState.lang) { this._donate.overlay.remove(); this._donate = null; } if (this._donate) { this._donate.overlay.classList.add('open'); return; } const overlay = mk('div', 'bm-overlay'); overlay.onclick = ev => { if (ev.target === overlay) overlay.classList.remove('open'); }; overlay.appendChild(this.donateStyle()); const modal = mk('div', 'bm-modal'); modal.classList.add('dn-modal'); modal.style.cssText = 'position:relative;height:auto;min-height:0;max-height:92vh;width:400px;max-width:94vw;animation:dnPop .22s ease'; modal.style.setProperty('background', 'var(--pb)', 'important'); const head = this.donateHead(tx); head.closeBtn.onclick = () => overlay.classList.remove('open'); const { el: toastEl, toast } = this.donateToast(); const icOpen = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6"/><path d="M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>'; const refRow = mk('div', 'dn-cta'); const refIc = mk('div', 'dn-ic'); refIc.style.background = 'rgba(0,0,0,.28)'; refIc.textContent = 'C'; const refMid = mk('div', 'dn-mid'); const refNm = mk('div', 'dn-nm'); refNm.textContent = 'Chaturbate'; const refSub = mk('div', 'dn-ad'); refSub.textContent = supT('sub'); refMid.append(refNm, refSub); const refOpen = mk('div'); refOpen.style.cssText = 'flex:none;display:flex'; refOpen.innerHTML = icOpen.replace('width="18" height="18"', 'width="22" height="22"'); refRow.title = supT('btn'); refRow.onclick = openSupportLink; refRow.append(refIc, refMid, refOpen); const refNote = mk('div', 'dn-note'); refNote.textContent = supT('note'); const walLbl = mk('div', 'dn-lbl'); walLbl.textContent = l.don_tip; const list = mk('div', 'dn-list'); this.donateWallets().forEach(w => list.appendChild(this.donateRow(w, tx, toast))); const hint = mk('div', 'dn-hint'); hint.textContent = tx.tap; const bodyEl = mk('div', 'dn-body'); bodyEl.append(refRow, refNote, walLbl, list, hint, toastEl); modal.append(bodyEl, head.el); overlay.appendChild(modal); this.shadow.appendChild(overlay); this._donate = { overlay, lang: AppState.lang }; requestAnimationFrame(() => overlay.classList.add('open')); } flash(btn, text) { const target = btn.querySelector('sub') || btn; const pending = !!target._bcFlashT; clearTimeout(target._bcFlashT); if (!pending) { target._bcOrig = target.innerHTML; target._bcPrevH = target.style.height; target._bcPrevO = target.style.overflow; const h = target.getBoundingClientRect().height; if (h > 0) { target.style.height = `${h}px`; target.style.overflow = 'visible'; } } const sz = target.tagName === 'SUB' ? 12 : 16; target.innerHTML = text === 'OK' ? `<svg viewBox="0 0 24 24" width="${sz}" height="${sz}" stroke="#2ecc71" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:middle;margin:0 auto"><polyline points="20 6 9 17 4 12"/></svg>` : text; target._bcFlashT = setTimeout(() => { target._bcFlashT = null; if (!target.isConnected) return; target.innerHTML = target._bcOrig; target.style.height = target._bcPrevH || ''; target.style.overflow = target._bcPrevO || ''; }, 1500); } ensureVisible() { try { if (!this.host) return; if (!this.host.isConnected) document.documentElement.appendChild(this.host); const cs = getComputedStyle(this.host); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') { ['display:block', 'visibility:visible', 'opacity:1'].forEach(d => { const [k, v] = d.split(':'); this.host.style.setProperty(k, v, 'important'); }); } if (bcPtrDown || realHidden() || window.innerWidth < 100 || window.innerHeight < 200) return; const el = AppState.isHidden ? this.ghostBtn : AppState.isMinimized ? this.restoreBtn : this.panel; const r = el.getBoundingClientRect(); const off = r.width < 1 || r.height < 1 || r.right < 24 || r.bottom < 24 || r.left > window.innerWidth - 24 || r.top > window.innerHeight - 24; if (off) this.resetPlacement(); } catch (e) { dbg(e, 'UIController.ensureVisible'); } } resetPlacement() { AppState.hasPos = false; Store.set('bc_pos', false); this.applyPosition(); } resetPanel() { this.toggleHide(false); this.toggleMinimize(false, true); AppState.scale = 1; Store.set('bc_scale', 1); this.panel.style.transform = 'scale(1)'; this.resetPlacement(); setTimeout(() => this.ensureVisible(), 300); } toggleHide(hide) { AppState.isHidden = hide; Store.set('bc_hid', hide); this.panel.classList.toggle('hidden', hide); this.ghostBtn.style.display = hide ? 'flex' : 'none'; if (!hide && AppState.hasPos) this.clampPosition(); } savePos(x, y) { AppState.posX = x; AppState.posY = y; AppState.hasPos = true; Store.set('bc_x', x); Store.set('bc_y', y); Store.set('bc_pos', true); } toggleMinimize(minimized, initial = false) { AppState.isMinimized = minimized; Store.set('bc_min', minimized); if (!initial) { const rect = this.panel.getBoundingClientRect(); const k = (minimized ? 1 : -1) * AppState.scale; const at = (p, d) => `${(parseFloat(this.panel.style[p]) || (p === 'top' ? rect.top - this.hostTop() : rect[p])) + k * d}px`; const newLeft = at('left', MINI_SHIFT_X), newTop = at('top', MINI_SHIFT_Y); this.panel.style.cssText += `;left:${newLeft};top:${newTop};right:auto;bottom:auto`; this.savePos(newLeft, newTop); } this.fullContainer.style.display = minimized ? 'none' : 'flex'; this.restoreBtn.style.display = minimized ? 'flex' : 'none'; this.restoreBtn.style.transformOrigin = 'top left'; this.restoreBtn.style.transform = `scale(${1 / AppState.scale})`; this.panel.className = minimized ? 'panel mini' : 'panel'; if (!initial) this.clampPosition(); } hostTop() { return this.host.getBoundingClientRect().top || 0; } onDragStart(e) { if (!isPrimaryPress(e)) return; e.preventDefault(); const rect = this.panel.getBoundingClientRect(); const iL = rect.left, iT = rect.top - this.hostTop(); this.panel.style.cssText += `;left:${iL}px;top:${iT}px;right:auto;bottom:auto`; this.panel.classList.add('dragging'); Helpers.trackDrag(e, { minDist: 2, capEl: this.panel, onMove: (dx, dy) => { this.hasMoved = true; this.panel.style.left = `${iL + dx}px`; this.panel.style.top = `${iT + dy}px`; }, onEnd: (moved) => { this.panel.classList.remove('dragging'); this.hasMoved = moved; if (moved) { this.clampPosition(); this.savePos(this.panel.style.left, this.panel.style.top); } } }); } setupDrag() { let swallowClick = false; this.panel.addEventListener('click', ev => { if (!swallowClick) return; swallowClick = false; ev.stopPropagation(); ev.preventDefault(); }, true); this.panel.addEventListener('pointerdown', e => { if (AppState.isMinimized) return; if (!isPrimaryPress(e)) return; if (e.target.closest('input, textarea, .vslider, .resize-handle, .bm-overlay')) return; if (!e.target.closest('button, a, .btn, .h-btn, .z-btn')) { this.onDragStart(e); return; } const sx = e.clientX, sy = e.clientY, pid = e.pointerId, tgt = e.target; const stop = () => { window.removeEventListener('pointermove', mv, true); window.removeEventListener('pointerup', stop, true); window.removeEventListener('pointercancel', stop, true); }; const mv = ev => { if (ev.pointerId !== pid || Math.hypot(ev.clientX - sx, ev.clientY - sy) < 10) return; stop(); swallowClick = true; window.addEventListener('pointerup', () => setTimeout(() => { swallowClick = false; }, 350), { once: true, capture: true }); try { tgt.dispatchEvent(new PointerEvent('pointercancel', { pointerId: pid, bubbles: true })); } catch (e) { dbg(e, 'setupDrag.mv'); } this.onDragStart(e); }; window.addEventListener('pointermove', mv, { capture: true, passive: true }); window.addEventListener('pointerup', stop, true); window.addEventListener('pointercancel', stop, true); }); } buildResizeHandles() { this.resizeHandle = mk('div', 'resize-handle'); this.resizeHandle.dataset.edge = 'se'; const edges = ['n', 's', 'e', 'w', 'nw', 'ne', 'sw'].map(k => { const d = mk('div', `rs-edge rs-${k}`); d.dataset.edge = k; return d; }); this.panel.append(this.resizeHandle, ...edges); return [this.resizeHandle, ...edges]; } startResize(e) { if (!isPrimaryPress(e)) return; e.preventDefault(); e.stopPropagation(); const edge = e.currentTarget.dataset.edge; const hasN = edge.includes('n'), hasS = edge.includes('s'), hasW = edge.includes('w'), hasE = edge.includes('e'); const rect = this.panel.getBoundingClientRect(); const s0 = AppState.scale, w0 = rect.width, h0 = rect.height; const scaleVal = this.shadow.querySelector('.s-val'); const ht = this.hostTop(); this.panel.style.left = `${rect.left}px`; this.panel.style.top = `${rect.top - ht}px`; this.panel.style.right = 'auto'; this.panel.style.bottom = 'auto'; let raf = 0, ns = s0; const paint = () => { raf = 0; const f = ns / s0; this.panel.style.transform = `scale(${ns})`; this.panel.style.left = `${hasW ? rect.right - w0 * f : rect.left}px`; this.panel.style.top = `${(hasN ? rect.bottom - h0 * f : rect.top) - ht}px`; if (scaleVal) scaleVal.textContent = `${Math.round(ns * 100)}%`; }; Helpers.trackDrag(e, { minDist: 1, onMove: (dx, dy) => { const kx = axisK(hasE, hasW, w0, dx), ky = axisK(hasS, hasN, h0, dy); ns = clamp(s0 * (kx !== null && ky !== null ? (kx + ky) / 2 : kx ?? ky), 0.6, 1.5); AppState.scale = ns; if (!raf) raf = requestAnimationFrame(paint); }, onEnd: moved => { if (!moved) return; if (raf) { cancelAnimationFrame(raf); paint(); } Store.set('bc_scale', ns); this.clampPosition(); this.savePos(this.panel.style.left, this.panel.style.top); } }); } setupResize() { this.buildResizeHandles().forEach(el => el.addEventListener('pointerdown', e => this.startResize(e))); } clampPosition(gap = 10) { if (this.panel.classList.contains('hidden')) return; if (realHidden() || window.innerHeight < 200 || window.innerWidth < 100) return; const rect = this.panel.getBoundingClientRect(); const l = parseFloat(this.panel.style.left), t = parseFloat(this.panel.style.top); if (isNaN(l) || isNaN(t)) return; this.panel.style.left = `${clamp(l, gap, Math.max(gap, window.innerWidth - gap - rect.width))}px`; const vh = window.innerHeight; this.panel.style.top = `${clamp(t, gap - vh, Math.max(gap - vh, -gap - rect.height))}px`; } applyPosition() { if (AppState.hasPos && AppState.posX && AppState.posY) { const py = parseFloat(AppState.posY); if (py >= 0 && window.innerHeight >= 200) { AppState.posY = `${Math.round(py - window.innerHeight)}px`; Store.set('bc_y', AppState.posY); } this.panel.style.left = AppState.posX; this.panel.style.top = AppState.posY; this.panel.style.right = 'auto'; this.panel.style.bottom = 'auto'; } else { this.panel.style.right = '15px'; this.panel.style.bottom = '10vh'; this.panel.style.left = 'auto'; this.panel.style.top = 'auto'; } } enforceBackgroundPlay() { if (this._pbBound) return; this._pbBound = true; const pb = e => { if (!(AppState.bgPlay && e.isTrusted)) return; e.stopImmediatePropagation(); const subs = BG_SUBS[e.type]; if (subs && !e._bcRelayed) { e._bcRelayed = true; subs.forEach(fn => { try { fn(e); } catch (e) { dbg(e, 'enforceBackgroundPlay.pb'); } }); } }; ['blur', 'visibilitychange', 'webkitvisibilitychange', 'pagehide'].forEach(evt => { window.addEventListener(evt, pb, true); document.addEventListener(evt, pb, true); }); const hid = Object.getOwnPropertyDescriptor(Document.prototype, 'hidden'); const vis = Object.getOwnPropertyDescriptor(Document.prototype, 'visibilityState'); try { Object.defineProperties(document, { hidden: { get: () => AppState.bgPlay ? false : hid.get.call(document), configurable: true }, visibilityState: { get: () => AppState.bgPlay ? 'visible' : vis.get.call(document), configurable: true } }); } catch (e) { dbg(e, 'UIController.enforceBackgroundPlay'); } } injectSiteCSS() { injectSiteCSS(); ensureHideCSS(); } } class BgSaver { constructor(videoController, recorder) { this.vc = videoController; this.rec = recorder; this.timer = 0; this.paused = null; this.lowered = null; onPageEvent('visibilitychange', () => this.onVis()); } busy() { return !!document.pictureInPictureElement || this.rec?.mRec?.state === 'recording'; } onVis() { clearTimeout(this.timer); if (realHidden()) { this.timer = setTimeout(() => this.enter(), 10000); } else { this.leave(); } } enter() { if (!realHidden() || this.busy()) return; try { if (AppState.bgPlay) { this.setLowest(); } else { const v = this.vc.cachedVideo; if (v && !v.paused) { v.pause(); this.paused = v; } } } catch (e) { dbg(e, 'BgSaver.enter'); } } leave() { try { const v = this.paused; this.paused = null; if (v?.isConnected) { const b = v.buffered; if (b.length) { const end = b.end(b.length - 1); if (end - v.currentTime > 3) v.currentTime = end - 0.5; } v.play().catch(() => {}); } if (this.lowered) { const h = this.lowered.prev; this.lowered = null; this.pickQuality(h); } } catch (e) { dbg(e, 'BgSaver.leave'); } } qualityItems() { const out = []; document.querySelectorAll('.js-qsb_list_wrp *, [class*="qsb_"] *, [class*="rendition"] *, [class*="quality_"] *, .vjs-menu-item *, [class*="resolution"] *, [class*="Resolution"] *, [role="menu"] *, [role="listbox"] *').forEach(el => { if (el.children.length || el.closest('button.player-resolution, button.vjs-icon-hd')) return; const m = (el.textContent || '').trim().match(/^(\d{3,4})\s*p?$/i); if (m) { const row = el.closest('li,a,button,div') || el; const mark = /(^|[\s_-])(active|selected|current|checked|__selected|__active)($|[\s_-])/; out.push({ el, h: +m[1], sel: mark.test(row.className) || mark.test(el.className) }); } }); return out; } openQualityMenu() { document.querySelector('.controls_current_rendition, #quality-btn, .player-resolution, button.vjs-icon-hd')?.click(); } setLowest() { this.openQualityMenu(); setTimeout(() => { try { const items = this.qualityItems(); if (items.length < 2) return; const low = items.reduce((a, b) => b.h < a.h ? b : a); const cur = items.find(i => i.sel); if (cur && cur.h === low.h) return; this.lowered = { prev: cur ? cur.h : Math.max(...items.map(i => i.h)) }; low.el.click(); } catch (e) { dbg(e, 'BgSaver.setLowest'); } }, 500); } pickQuality(h) { this.openQualityMenu(); setTimeout(() => { try { this.qualityItems().find(i => i.h === h)?.el.click(); } catch (e) { dbg(e, 'BgSaver.pickQuality'); } }, 500); } } const autoAge = () => { if (!BC_RE.test(location.hostname)) return; const sel = '.js-18_plus_warning a.js-close_warning, #popup_18_plus a.button_18_plus'; let clicked = false; const cleanup = () => { try { document.body?.classList.remove('bd_18plus'); document.documentElement.classList.remove('ht_18plus'); } catch (e) { dbg(e, 'autoAge.cleanup'); } }; const accept = () => { const a = document.querySelector(sel); if (!a) return false; if (clicked) return true; clicked = true; a.addEventListener('click', e => e.preventDefault(), { once: true, capture: true }); try { a.click(); } catch (e) { dbg(e, 'autoAge.accept'); } const done = () => setTimeout(cleanup, 150); try { fetch(a.href, { credentials: 'include', cache: 'no-store' }).then(done, done); } catch { done(); } return true; }; if (accept()) return; const mo = new MutationObserver(() => { if (accept()) mo.disconnect(); }); mo.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(() => mo.disconnect(), 20000); }; autoAge(); const VIEWERS_SEL = '#bChatHeader .js-oc_count, .chat-header-inner .js-oc_count, #online-chat .js-oc_count, .chat-info-viewers, .v-viewers, .js-oc_count, .oc_count, .player-viewers-count, .HeaderRoomInfo__numViewers, [data-testid="viewers-count"]'; const VIEWERS_FALLBACK = '[class*="numViewers" i], [class*="viewerCount" i], [class*="viewersCount" i], [class*="viewers-count" i], [data-testid*="viewer" i]'; const readViewers = () => { const pick = sel => { try { for (const el of document.querySelectorAll(sel)) { const t = (el.textContent || '').trim(); if (t && t.length < 60 && /\d/.test(t)) return t; } } catch (e) { dbg(e, 'readViewers.pick'); } return ''; }; return pick(VIEWERS_SEL) || pick(VIEWERS_FALLBACK); }; const markFav = (btn, on) => { if (!btn) return; btn.style.background = ''; btn.style.borderColor = ''; btn.classList.toggle('fav-on', !!on); }; const SLEEP_OPTS = [0, 15, 30, 45, 60, 90, 120]; const SLEEP_CSS = `.rs-pult .sl-r{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px} .rs-pult .sl-f{display:flex;flex-direction:column;align-items:center;gap:4px;min-width:0;padding:8px 6px;border-radius:12px;border:1px solid var(--pl);background:var(--pt)} .rs-pult .sl-f span{font-size:12px;opacity:.62} .rs-pult .sl-f input{width:100%;min-width:0;margin:0;padding:2px 0;border:0;outline:0;background:transparent;color:#fff;text-align:center;font:600 26px/1.15 Onest,system-ui,sans-serif;-moz-appearance:textfield;appearance:textfield} .rs-pult .sl-f input::-webkit-outer-spin-button,.rs-pult .sl-f input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0} .rs-pult .sl-pm{display:flex;gap:6px;width:100%} .rs-pult .sl-pm button{flex:1;min-height:40px;margin:0;padding:0;border:0;border-radius:9px;background:var(--pb);color:#fff;font:500 20px/1 Onest,system-ui,sans-serif;cursor:pointer;transition:transform .12s} .rs-pult .sl-pm button:active,.rs-pult .sl-go:active{transform:scale(.96)} .rs-pult .sl-go{display:flex;align-items:center;justify-content:center;width:100%;min-height:48px;margin:4px 0 0;padding:0 12px;border:0;border-radius:12px;background:var(--th);color:var(--pon);font:600 15px/1.2 Onest,system-ui,sans-serif;cursor:pointer;transition:transform .12s} .rs-pult .sl-pm button:focus-visible,.rs-pult .sl-go:focus-visible,.rs-pult .sl-f input:focus-visible{outline:2px solid var(--th);outline-offset:2px}`; class SleepTimer { constructor(ui, rec, bg) { this.ui = ui; this.rec = rec; this.bg = bg; this.lock = false; this.guard = 0; this.cover = null; this.mo = null; this.end = 0; this.mins = 0; this.t = 0; this.iv = 0; this.modal = null; this.at = 0; const end = +Store.get('bc_sleep_end', 0) || 0; if (end > Date.now()) { this.mins = +Store.get('bc_sleep_min', 0) || 0; this.arm(end); } else if (end) { Store.set('bc_sleep_end', 0); Store.set('bc_sleep_min', 0); } const lockAt = +Store.get('bc_sleep_lock', 0) || 0; if (lockAt && Date.now() - lockAt < 7200000) this.engage(); else if (lockAt) Store.set('bc_sleep_lock', 0); ['play', 'playing', 'loadstart', 'loadedmetadata', 'canplay'].forEach(t => document.addEventListener(t, e => this.onPlay(e), true)); document.addEventListener('timeupdate', () => this.check(), true); onPageEvent('visibilitychange', () => this.check()); } left() { return Math.max(0, this.end - Date.now()); } fmt() { const t = Math.ceil(this.left() / 1000), p = n => String(n).padStart(2, '0'); return t >= 3600 ? `${Math.floor(t / 3600)}:${p(Math.floor(t / 60) % 60)}:${p(t % 60)}` : Helpers.formatTime(t); } set(min) { this.setMs(min * 60000); } setMs(ms) { if (!(ms > 0)) return this.cancel(); ms = Math.min(Math.round(ms), 86400000); this.mins = ms / 60000; const end = Date.now() + ms; Store.set('bc_sleep_end', end); Store.set('bc_sleep_min', this.mins); this.arm(end); } arm(end) { clearTimeout(this.t); clearInterval(this.iv); this.end = end; this.t = setTimeout(() => this.check(), Math.max(0, end - Date.now()) + 50); this.iv = setInterval(() => { this.paint(); this.check(); }, 1000); this.paint(); } cancel() { clearTimeout(this.t); clearInterval(this.iv); this.t = this.iv = 0; this.end = 0; this.mins = 0; Store.set('bc_sleep_end', 0); Store.set('bc_sleep_min', 0); this.paint(); } check() { if (this.end && Date.now() >= this.end) this.fire(); } engage() { this.lock = true; this.strip(); this.showCover(); clearInterval(this.guard); this.guard = setInterval(() => this.strip(), 2000); if (!this.mo) { const hasMedia = n => n.nodeType === 1 && (n.matches('video,audio') || !!n.querySelector('video,audio')); this.mo = new MutationObserver(recs => { for (const r of recs) for (const n of r.addedNodes) if (hasMedia(n)) return this.strip(); }); this.mo.observe(document.documentElement, { childList: true, subtree: true }); } } unlock() { this.lock = false; clearInterval(this.guard); this.guard = 0; this.mo?.disconnect(); this.mo = null; this.cover?.remove(); this.cover = null; Store.set('bc_sleep_lock', 0); } wake() { this.unlock(); location.reload(); } strip() { try { window.dispatchEvent(new Event('bc_sleep_kill')); } catch (e) { dbg(e, 'SleepTimer.strip'); } try { document.querySelectorAll('video,audio').forEach(v => { try { v.pause(); v.srcObject?.getTracks?.().forEach(t => t.stop()); v.srcObject = null; v.removeAttribute('src'); v.load(); } catch (e) { dbg(e, 'SleepTimer.strip'); } v.remove(); }); } catch (e) { dbg(e, 'SleepTimer.strip'); } } showCover() { if (this.cover?.isConnected) return; const c = mk('div', ''); c.style.cssText = 'position:fixed;inset:0;z-index:0;background:#000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center;padding:24px;color:#8a8a90;font-size:15px;pointer-events:auto;touch-action:manipulation;cursor:pointer'; String(L().sleep_wake).split('\n').forEach(t => c.append(textEl('', t, 'div'))); c.onclick = () => this.wake(); this.ui.shadow.insertBefore(c, this.ui.shadow.firstChild); this.cover = c; } onPlay(e) { if (!this.lock || !(e.target instanceof HTMLMediaElement)) return; this.strip(); } fire() { this.cancel(); Store.set('bc_sleep_lock', Date.now()); let wasRec = false; try { const btn = this.ui.elements.bRec; if (this.rec?.mRec && this.rec.mRec.state !== 'inactive') { wasRec = true; this.rec.stop(btn, btn.querySelector('sub')); } } catch (e) { dbg(e, 'SleepTimer.fire'); } if (this.bg) this.bg.paused = null; try { this.ui.flash(this.ui.elements.bBg, 'OK'); } catch (e) { dbg(e, 'SleepTimer.fire'); } if (wasRec) setTimeout(() => { if (Store.get('bc_sleep_lock', 0)) this.engage(); }, 2000); else this.engage(); } paint() { const sub = this.ui.elements.bBg?.querySelector('sub'); if (sub && !sub._bcFlashT) { if (this.end) { sub._slp = true; sub.textContent = this.fmt(); } else if (sub._slp) { sub._slp = false; sub.textContent = L().bg; } } this.paintModal(); } clock(ms) { const d = new Date(Date.now() + ms), p = n => String(n).padStart(2, '0'); return `${p(d.getHours())}:${p(d.getMinutes())}`; } paintModal() { const el = this.modal?.sumTx; if (!el || !el.isConnected) return; const l = L(); el.textContent = this.end ? `${l.sleep_left}: ${this.fmt()} · → ${this.clock(this.left())}` : l.rs_off; } open() { if (!this.modal) { const { overlay, modal, title, closeBtn, body } = buildModalShell(this.ui.shadow, 'bm-modal rs-modal rs-pult'); modal.style.setProperty('background', 'var(--pb)', 'important'); closeBtn.innerHTML = ICON_CLOSE; const css = document.createElement('style'); css.textContent = RS_PULT_CSS + SLEEP_CSS; overlay.appendChild(css); const close = () => { overlay.classList.remove('open'); overlay._fromSt = false; overlay._bk?.(); }; closeBtn.onclick = close; overlay.onclick = e => { if (e.target === overlay && Date.now() - this.at > 400) close(); }; this.modal = { overlay, title, body, close, sumTx: null }; } this.at = Date.now(); this.render(); this.modal.overlay.classList.add('open'); } sleepField(unit, max) { const wrap = mk('div', 'sl-f'); const inp = mk('input', ''); inp.type = 'number'; inp.min = '0'; inp.max = String(max); inp.step = '1'; inp.value = '0'; inp.inputMode = 'numeric'; inp.onfocus = () => inp.select(); const num = () => Math.min(max, Math.max(0, parseInt(inp.value, 10) || 0)); const mkBtn = (txt, d) => { const b = mk('button', ''); b.type = 'button'; b.textContent = txt; b.onclick = () => { inp.value = String(Math.min(max, Math.max(0, num() + d))); inp.dispatchEvent(new Event('input')); }; return b; }; const pm = mk('div', 'sl-pm'); pm.append(mkBtn('−', -1), mkBtn('+', 1)); wrap.append(textEl('', unit, 'span'), inp, pm); return { wrap, inp, num }; } render() { const m = this.modal; if (!m) return; const l = L(); m.title.innerHTML = `<span class="rs-ic">${icons.sleep}</span><span>${escHTML(l.sleep_title)}</span>`; const sum = mk('div', 'rs-sum'); sum.innerHTML = icons.sleep; m.sumTx = mk('span'); sum.appendChild(m.sumTx); const off = mk('div', 'rs-chips'); const offBtn = mk('button', `bm-tbtn${this.end ? '' : ' on'}`); offBtn.textContent = l.rs_off; offBtn.onclick = () => { this.set(0); m.close(); }; off.append(offBtn); const qs = mk('div', 'rs-qs'); SLEEP_OPTS.filter(v => v).forEach(v => { const hrs = v >= 60 && v % 60 === 0; const b = mk('button', `bm-btn${this.end && v === this.mins ? ' sel' : ''}`); const r1 = mk('b'); r1.textContent = hrs ? v / 60 : v; const r2 = mk('span'); r2.textContent = hrs ? l.u_h : l.u_m; const r3 = mk('small'); r3.textContent = `→ ${this.clock(v * 60000)}`; b.append(r1, r2, r3); b.onclick = () => { this.set(v); m.close(); }; qs.append(b); }); const fh = this.sleepField(l.u_h, 24), fm = this.sleepField(l.u_m, 1440), fs = this.sleepField(l.sleep_sec, 86400); const row = mk('div', 'sl-r'); row.append(fh.wrap, fm.wrap, fs.wrap); const go = mk('button', 'sl-go'); go.type = 'button'; const total = () => (fh.num() * 3600 + fm.num() * 60 + fs.num()) * 1000; const paintGo = () => { const ms = Math.min(total(), 86400000), t = Math.round(ms / 1000), p = n => String(n).padStart(2, '0'); go.textContent = ms ? `${l.sleep_start} · ${p(Math.floor(t / 3600))}:${p(Math.floor(t / 60) % 60)}:${p(t % 60)} → ${this.clock(ms)}` : l.sleep_start; }; const start = () => { const ms = total(); if (!ms) return; this.setMs(ms); m.close(); }; go.onclick = start; [fh, fm, fs].forEach(f => { f.inp.oninput = paintGo; f.inp.onkeydown = e => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); start(); } }; }); paintGo(); const grp = (...kids) => { const g = mk('div', 'rs-g'); g.append(...kids); return g; }; m.body.replaceChildren( sum, grp(off, qs), grp(textEl('hd-sec', l.sleep_custom), row, go), textEl('rs-note', l.sleep_hint) ); this.paintModal(); } } const RAD = { NODE: 42, R0: 84, RSTEP: 62, DEAD: 26, ARC: 0.44, HOLD: 380, FAB: 48 }; const radDiff = (a, b) => { let d = (a - b) % (Math.PI * 2); if (d > Math.PI) d -= Math.PI * 2; if (d < -Math.PI) d += Math.PI * 2; return d; }; const RAD_CSS = `.rm-fab{position:fixed;width:48px;height:48px;border-radius:50%;display:none;align-items:center;justify-content:center;color:#fff;background:var(--th);border:2px solid rgba(255,255,255,.35);box-shadow:0 4px 15px rgba(0,0,0,.6);pointer-events:auto;touch-action:none!important;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;cursor:pointer;opacity:.85;z-index:2147483640;transition:transform .15s,opacity .15s,box-shadow .15s} .rm-fab.on{display:flex}.rm-fab:hover{opacity:1}.rm-fab.moving{transform:scale(1.25);opacity:1;box-shadow:0 0 0 6px rgba(255,255,255,.2),0 8px 24px rgba(0,0,0,.7)}.rm-fab svg{width:24px;height:24px;pointer-events:none} .rm-ov{position:fixed;inset:0;display:none;pointer-events:none;touch-action:none;z-index:2147483645;background:rgba(0,0,0,.38)}.rm-ov.open{display:block}.rm-ov.sticky{pointer-events:auto} .rm-stage{position:absolute;width:0;height:0;transform-origin:0 0} .rm-ring{position:absolute;border-radius:50%;box-sizing:content-box;border-style:solid;border-color:rgba(255,255,255,.07);transform:translate(-50%,-50%);pointer-events:none;z-index:0} .rm-hub{position:absolute;z-index:1;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;background:rgba(15,15,18,.95);border:2px solid rgba(255,255,255,.25);color:#fff;font:700 10px/1.1 system-ui,sans-serif;display:flex;align-items:center;justify-content:center;text-align:center;padding:4px;overflow:hidden;word-break:break-word;pointer-events:auto;cursor:pointer;transition:background .1s} .rm-hub.lit{background:var(--th)}.rm-hub svg{width:24px;height:24px} .rm-node{position:absolute;z-index:2;width:42px;height:42px;margin:-21px 0 0 -21px;border-radius:50%;background:rgba(15,15,18,.95);border:1px solid rgba(255,255,255,.2);color:#fff;display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;box-shadow:0 3px 10px rgba(0,0,0,.5);animation:rmPop .14s ease-out;transition:transform .1s,background .1s} .rm-node svg{width:20px;height:20px;pointer-events:none} .rm-node.grp::after{content:'';position:absolute;right:2px;bottom:2px;width:8px;height:8px;border-radius:50%;background:var(--th);border:1px solid #fff} .rm-node.hot{background:var(--th);transform:scale(1.28);border-color:#fff;z-index:3} @keyframes rmPop{from{transform:scale(.3);opacity:0}to{transform:scale(1);opacity:1}}`; const nearestAngle = (pos, ang) => { let best = -1, bd = 1e9; pos.forEach((p, i) => { const df = Math.abs(radDiff(ang, p.a)); if (df < bd) { bd = df; best = i; } }); return { best, bd }; }; class RadialMenu { constructor(ui, app) { this.ui = ui; this.app = app; this.on = Store.get('bc_rad', false) === true; this.mode = ''; this.path = []; this.lv = []; this.items = []; this.scale = 1; const st = document.createElement('style'); st.textContent = RAD_CSS; ui.shadow.appendChild(st); this.fab = mk('div', 'rm-fab'); this.fab.innerHTML = icons.target; this.ov = mk('div', 'rm-ov'); this.stage = mk('div', 'rm-stage'); this.hubEl = mk('div', 'rm-hub'); this.ov.appendChild(this.stage); ui.shadow.append(this.ov, this.fab); this.bind(); this.placeFab(); this.fab.classList.toggle('on', this.on); window.addEventListener('resize', () => { this.close(); this.placeFab(); }, { passive: true }); window.addEventListener('keydown', e => { if (e.key === 'Escape' && this.mode) this.close(); }, true); } setEnabled(v) { this.on = !!v; Store.set('bc_rad', this.on); this.fab.classList.toggle('on', this.on); if (!this.on) this.close(); this.placeFab(); } resetPos() { Store.set('bc_rad_x', null); Store.set('bc_rad_y', null); this.placeFab(); } fabPos() { const W = window.innerWidth, H = window.innerHeight, S = RAD.FAB; const x = parseFloat(Store.get('bc_rad_x', null)), y = parseFloat(Store.get('bc_rad_y', null)); return { x: clamp(Number.isFinite(x) ? x : W - S - 14, 4, Math.max(4, W - S - 4)), y: clamp(Number.isFinite(y) ? y : Math.round(H * 0.55), 4, Math.max(4, H - S - 4)) }; } placeFab() { const p = this.fabPos(); this.fab.style.left = `${p.x}px`; this.fab.style.top = `${p.y}px`; } treeCtx() { const l = L(), ui = this.ui, app = this.app, vc = app.videoController, el = ui.elements; const press = b => { if (!b) return; if (b.onpointerup) { const o = { button: 0, pointerType: 'touch', isPrimary: true, bubbles: true }; b.dispatchEvent(new PointerEvent('pointerdown', o)); b.dispatchEvent(new PointerEvent('pointerup', o)); } else b.click(); }; const vol = d => () => { const v = vc.cachedVideo; if (v) vc.setVolume(v.volume + d); }; const addFav = () => { const name = currentSite.getName(); if (name && !isFavName(name)) { app.bookmarks.add(); ui.flash(el.bFav, 'OK'); app.updateViewersCount(); } else ui.flash(el.bFav, 'ERR'); }; return { l, ui, app, vc, el, press, vol, addFav }; } treeMedia(c) { const { l, app, vc, el, press, vol } = c; return { icon: icons.vol, label: l.rad_media, kids: [ { icon: icons.bgOn, label: l.bg, run: () => press(el.bBg) }, { icon: icons.mute, label: l.sound, run: () => press(el.bMute) }, { icon: icons.volMed, label: l.rad_vup, run: vol(0.1) }, { icon: icons.volLow, label: l.rad_vdn, run: vol(-0.1) }, { icon: icons.snap, label: l.snap, run: () => vc.takeScreenshot() }, { icon: icons.rec, label: l.rec, run: () => app.recorder.toggle() } ] }; } treeView(c) { const { l, ui, vc } = c; return { icon: icons.fs, label: l.rad_view, kids: [ { icon: icons.qual, label: l.qual, run: () => ui.toggleQuality() }, { icon: icons.pip, label: l.pip, run: () => vc.togglePiP() }, { icon: icons.fs, label: l.fs, run: () => vc.toggleFullscreen() }, { icon: icons.thtr, label: l.thtr, run: () => vc.toggleTheater() } ] }; } treeNav(c) { const { l, app, el, addFav } = c; return { icon: icons.fav, label: l.rad_nav, kids: [ { icon: icons.search, label: l.search, run: () => app.search.open() }, { icon: icons.bookmark, label: l.favs, run: () => app.bookmarks.open() }, { icon: icons.fav, label: l.bm_add, run: addFav }, { icon: icons.favPrev, label: l.rad_prev, run: () => el.bFavPrev?.click() }, { icon: icons.favNext, label: l.rad_next, run: () => el.bFavNext?.click() }, { icon: icons.copy, label: l.copy, run: () => el.bCopy?.click() } ] }; } treePage(c) { const { l, ui, app } = c; return { icon: icons.grid, label: l.rad_page, kids: [ { icon: icons.home, label: l.home, run: () => { location.href = '/'; } }, { icon: icons.rel, label: l.rel, run: () => location.reload() }, { icon: AppState.dark ? icons.sun : icons.moon, label: l.dark, run: () => ui.toggleDark() }, { icon: icons.sleep, label: l.sleep_title, run: () => app.sleep.open() }, { icon: icons.history, label: l.hist_title, run: () => app.history.open() }, { icon: icons.min, label: l.min, run: () => ui.toggleMinimize(true) }, { icon: icons.close, label: l.hide, run: () => ui.toggleHide(true) } ] }; } tree() { const c = this.treeCtx(), { l, app } = c; return [ this.treeMedia(c), this.treeView(c), this.treeNav(c), this.treePage(c), { icon: icons.gear, label: l.settings, run: () => app.settings.open() } ]; } fan(r) { const { x: hx, y: hy } = this.hub, W = window.innerWidth, H = window.innerHeight, M = RAD.NODE / 2 + 4, b = this.base; const ok = a => { const x = hx + Math.cos(a) * r, y = hy + Math.sin(a) * r; return x >= M && x <= W - M && y >= M && y <= H - M; }; let lo = 0, hi = 0; const st = 0.04; while (hi < Math.PI && ok(b + hi + st)) hi += st; while (lo > -Math.PI && ok(b + lo - st)) lo -= st; return { lo, hi, full: hi - lo >= Math.PI * 2 - 0.12 }; } spread(c, n, step) { return Array.from({ length: n }, (_, i) => c + (i - (n - 1) / 2) * step); } layoutRoot(n, f, b) { if (f.full) { const step = (Math.PI * 2) / n; return { step, angles: Array.from({ length: n }, (_, i) => -Math.PI / 2 + i * step) }; } const step = n > 1 ? Math.min(1.3, (f.hi - f.lo) / (n - 1)) : 0; return { step, angles: this.spread(b + (f.lo + f.hi) / 2, n, step) }; } layoutKids(n, f, b, parentAng) { if (f.full) return { step: RAD.ARC, angles: this.spread(parentAng, n, RAD.ARC) }; const span = f.hi - f.lo; let step = RAD.ARC; if ((n - 1) * step > span) step = n > 1 ? span / (n - 1) : 0; const w = ((n - 1) * step) / 2; const c = b + clamp(radDiff(parentAng, b), f.lo + w, f.hi - w); return { step, angles: this.spread(c, n, step) }; } layout(list, d, parentAng) { const r = RAD.R0 + d * RAD.RSTEP, n = list.length, b = this.base; const { step, angles } = d === 0 ? this.layoutRoot(n, this.f0, b) : this.layoutKids(n, this.f1, b, parentAng); const out = angles.map(a => ({ a, x: Math.cos(a) * r, y: Math.sin(a) * r })); out.step = step; return out; } kidsHit(list, pos, ang, dist) { if (dist < RAD.R0 + RAD.RSTEP - RAD.NODE / 2 - 8) return false; const c = this.path[0], cn = c != null ? list[c] : null; if (!cn || !cn.kids) return false; const kp = this.layout(cn.kids, 1, pos[c].a), tol = Math.max(kp.step, 0.2) * 0.6; return kp.some(p => Math.abs(radDiff(ang, p.a)) <= tol); } beyondRing(d, dist) { return d > 0 && dist < RAD.R0 + d * RAD.RSTEP - RAD.NODE / 2 - 8; } tooFar(d, pos, bd) { return d > 0 && bd > Math.max(pos.step, 0.2) * 0.6; } pick(vx, vy) { const dist = Math.hypot(vx, vy); if (dist < RAD.DEAD) return []; const ang = Math.atan2(vy, vx), path = []; let list = this.items, parentAng = null; for (let d = 0; list && !this.beyondRing(d, dist); d++) { const pos = this.layout(list, d, parentAng); let { best, bd } = nearestAngle(pos, ang); if (d === 0 && this.kidsHit(list, pos, ang, dist)) best = this.path[0]; if (this.tooFar(d, pos, bd)) break; path.push(best); const node = list[best]; if (!node.kids) break; parentAng = pos[best].a; list = node.kids; } return path; } moveFab(f, e) { const S = RAD.FAB; f.style.left = `${clamp(e.clientX - S / 2, 4, window.innerWidth - S - 4)}px`; f.style.top = `${clamp(e.clientY - S / 2, 4, window.innerHeight - S - 4)}px`; } dragMove(st, e) { const vx = e.clientX - st.ox, vy = e.clientY - st.oy; if (!st.moved && Math.hypot(vx, vy) > 10) { st.moved = true; clearTimeout(st.timer); } if (st.moved) this.setPath(this.pick(vx / this.scale, vy / this.scale)); if (e.cancelable) e.preventDefault(); } endMove(f) { f.classList.remove('moving'); this.mode = ''; Store.set('bc_rad_x', f.style.left); Store.set('bc_rad_y', f.style.top); } endDrag(e, s) { if (e.type === 'pointercancel') return this.close(); const node = this.nodeAtPath(this.path); if (node && !node.kids) { this.close(); this.run(node); return; } if (node || !s.moved) return this.sticky(); this.close(); } nodeAtPath(path) { let list = this.items, node = null; for (const i of path) { node = list && list[i] || null; list = node && node.kids; } return node; } openMenu(c, mode) { this.items = this.tree(); this.path = []; this.scale = 1; this.hub = { x: c.x, y: c.y }; const cx = window.innerWidth / 2, cy = window.innerHeight / 2; this.base = Math.hypot(cx - c.x, cy - c.y) < 20 ? -Math.PI / 2 : Math.atan2(cy - c.y, cx - c.x); this.f0 = this.fan(RAD.R0); this.f1 = this.fan(RAD.R0 + RAD.RSTEP); this.stage.style.cssText = `left:${c.x}px;top:${c.y}px`; this.lv = []; this.stage.replaceChildren(this.hubEl); this.mode = mode; this.ov.className = `rm-ov open${mode === 'sticky' ? ' sticky' : ''}`; this.render(); this.updateHub(); } buildLevel({ list, pos, key, d }) { const el = mk('div', 'rm-lvl'); const r = RAD.R0 + d * RAD.RSTEP, band = RAD.NODE + 12; const ring = mk('div', 'rm-ring'); ring.style.cssText = `width:${2 * r - band}px;height:${2 * r - band}px;border-width:${band}px`; el.appendChild(ring); const nodes = list.map((it, i) => { const n = mk('div', `rm-node${it.kids ? ' grp' : ''}`); n.innerHTML = it.icon; n.title = it.label; n.dataset.d = d; n.dataset.i = i; n.style.left = `${pos[i].x}px`; n.style.top = `${pos[i].y}px`; el.appendChild(n); return n; }); this.stage.appendChild(el); return { key, list, nodes, el }; } render() { const levels = []; let list = this.items, parentAng = null, key = ''; for (let d = 0; list; d++) { const pos = this.layout(list, d, parentAng); levels.push({ list, pos, key, d }); const i = this.path[d]; if (i == null || !list[i] || !list[i].kids) break; parentAng = pos[i].a; key += `/${i}`; list = list[i].kids; } let from = levels.findIndex((lv, d) => !this.lv[d] || this.lv[d].key !== lv.key); if (from < 0) from = levels.length; while (this.lv.length > from) this.lv.pop().el.remove(); for (let d = from; d < levels.length; d++) this.lv.push(this.buildLevel(levels[d])); this.lv.forEach((lv, d) => lv.nodes.forEach((n, i) => n.classList.toggle('hot', this.path[d] === i))); } updateHub() { const n = this.nodeAtPath(this.path); this.hubEl.classList.toggle('lit', !!n); if (n) this.hubEl.textContent = n.label; else this.hubEl.innerHTML = icons.target; } setPath(p) { if (p.length === this.path.length && p.every((v, i) => v === this.path[i])) return; this.path = p; this.render(); this.updateHub(); if (p.length && navigator.vibrate) navigator.vibrate(8); } sticky() { this._stAt = Date.now(); this.mode = 'sticky'; this.ov.classList.add('sticky'); } close() { if (!this.mode) return; this.mode = ''; this.ov.className = 'rm-ov'; this.path = []; this.lv = []; this.stage.replaceChildren(); } run(it) { try { it.run(); } catch (err) { console.warn('[BCU radial]', err); } } bind() { this.bindFabGuards(); this.bindFabPointer(); } bindFabGuards() { const f = this.fab; f.oncontextmenu = e => e.preventDefault(); ['touchstart', 'touchmove', 'touchend'].forEach(t => f.addEventListener(t, e => { if (e.cancelable) e.preventDefault(); }, { passive: false })); ['touchstart', 'touchmove'].forEach(t => this.ov.addEventListener(t, e => { if (e.cancelable) e.preventDefault(); }, { passive: false })); window.addEventListener('contextmenu', e => { const path = e.composedPath ? e.composedPath() : []; if (this.mode || path.includes(f) || path.includes(this.ov)) { e.preventDefault(); e.stopImmediatePropagation(); } }, true); } bindFabPointer() { const f = this.fab; let st = null; f.onpointerdown = e => { if (!isPrimaryPress(e)) return; swallow(e); this.close(); try { f.setPointerCapture(e.pointerId); } catch (e) { dbg(e, 'RadialMenu.bindFabPointer'); } const p = this.fabPos(); st = { id: e.pointerId, ox: e.clientX, oy: e.clientY, moved: false, timer: 0 }; this.openMenu({ x: p.x + RAD.FAB / 2, y: p.y + RAD.FAB / 2 }, 'drag'); st.timer = setTimeout(() => { if (!st || st.moved || this.mode !== 'drag') return; this.close(); this.mode = 'move'; f.classList.add('moving'); if (navigator.vibrate) navigator.vibrate(15); }, RAD.HOLD); }; f.onpointermove = e => { if (!st || e.pointerId !== st.id) return; if (this.mode === 'move') return this.moveFab(f, e); if (this.mode !== 'drag') return; this.dragMove(st, e); }; const end = e => { if (!st || e.pointerId !== st.id) return; clearTimeout(st.timer); const s = st; st = null; try { f.releasePointerCapture(e.pointerId); } catch (e) { dbg(e, 'bindFabPointer.end'); } if (this.mode === 'move') return this.endMove(f); if (this.mode !== 'drag') return; this.endDrag(e, s); }; f.onpointerup = end; f.onpointercancel = end; f.onlostpointercapture = e => { if (st && e.pointerId === st.id) end({ type: 'pointercancel', pointerId: e.pointerId }); }; window.addEventListener('blur', () => { if (st) end({ type: 'pointercancel', pointerId: st.id }); }); document.addEventListener('visibilitychange', () => { if (document.hidden && st) end({ type: 'pointercancel', pointerId: st.id }); }); this.bindStickyUp(); } bindStickyUp() { this.ov.onpointerup = e => { if (this.mode !== 'sticky' || !isPrimaryPress(e) || Date.now() - (this._stAt || 0) < 150) return; swallow(e); const n = e.target.closest ? e.target.closest('.rm-node') : null; if (!n) return this.close(); this.stickyPick(+n.dataset.d, +n.dataset.i); }; } stickyPick(d, i) { const it = this.lv[d] && this.lv[d].list[i]; if (!it) return; if (!it.kids) { this.close(); this.run(it); return; } const same = this.path[d] === i && this.path.length === d + 1; this.setPath(same ? this.path.slice(0, d) : this.path.slice(0, d).concat(i)); } } class App { constructor() { this.createModules(); const vc = this.videoController, el = this.ui.elements; this.bindMediaButtons(vc, el); this.bindFavButtons(vc, el); this.restoreUiState(); this.bindRemoteAndWindow(vc); this.startTimers(); } createModules() { this.ui = new UIController(); this.videoController = new VideoController(this.ui); this.recorder = new MediaRecorderManager(this.videoController, this.ui); this.bgSaver = new BgSaver(this.videoController, this.recorder); this.bookmarks = new BookmarkManager(this.ui); this.search = new SearchManager(this.ui); this.ui.bookmarkManager = this.bookmarks; this.ui.searchManager = this.search; this.hideManager = new HideManager(this.ui); this.ui.hideManager = this.hideManager; this.multiView = new MultiView(this.ui); this.ui.multiView = this.multiView; bmDropHooks.push(k => this.multiView.dropBmKey(k)); this.history = new HistoryManager(this.ui, this.bookmarks); this.thumbStars = new ThumbStars(this.bookmarks); this.ui.historyManager = this.history; this.calendar = new CalendarManager(this.ui, this.bookmarks); this.ui.calendarManager = this.calendar; this.ui.thumbStars = this.thumbStars; this.ui.recorder = this.recorder; this.settings = new SettingsManager(this.ui); this.ui.settingsManager = this.settings; this.sleep = new SleepTimer(this.ui, this.recorder, this.bgSaver); this.ui.sleepTimer = this.sleep; this.ui.radial = new RadialMenu(this.ui, this); } bindMediaButtons(vc, el) { const bgClick = el.bBg.onclick; el.bBg.onclick = null; Helpers.bindLongPress(el.bBg, () => bgClick.call(el.bBg), () => this.sleep.open()); Helpers.bindLongPress(el.bMute, () => { if (AppState.gmute) setGMute(false); vc.toggleMute(); }, () => { setGMute(true); const v = vc.cachedVideo; if (v) { v.muted = true; v.volume = 0; } this.ui.flash(el.bMute, 'LOCK'); vc.syncUI(); } ); el.bPip.onclick = () => vc.togglePiP(); el.bFs.onclick = () => vc.toggleFullscreen(); el.bThtr.onclick = () => vc.toggleTheater(); el.bShot.onclick = () => vc.takeScreenshot(); el.vIco.onclick = () => vc.toggleMute(); Helpers.bindLongPress(el.bRec, () => this.recorder.toggle(), () => this.recorder.openSettings()); el.bSearch.onclick = () => this.search.open(); } async goFav(dir, btn) { if (btn._busy) return; btn._busy = true; btn.style.opacity = '.5'; try { await this.bookmarks.checkStatuses(); const here = siteKey(location.hostname); const curName = currentSite.getName(); const list = AppState.bookmarks .filter(b => b.status === 1 && isSafeUrl(b.url) && urlSite(b.url) === here) .sort((x, y) => (!!y.pin - !!x.pin) || x.name.localeCompare(y.name)); const idx = list.findIndex(b => String(b.name).toLowerCase() === String(curName).toLowerCase()); if (!list.length || (list.length === 1 && idx === 0)) return this.ui.flash(btn, 'ERR'); const next = idx < 0 ? (dir > 0 ? list[0] : list[list.length - 1]) : list[(idx + dir + list.length) % list.length]; location.href = linkFor(next.url); } catch { this.ui.flash(btn, 'ERR'); } finally { btn._busy = false; btn.style.opacity = ''; } } bindFavButtons(vc, el) { el.bFavPrev.onclick = () => this.goFav(-1, el.bFavPrev); el.bFavNext.onclick = () => this.goFav(1, el.bFavNext); el.bFavPrev.title = el.bFavNext.title = ''; el.vSlider.oninput = e => vc.setVolume(parseFloat(e.target.value)); el.vSlider.onwheel = e => { e.preventDefault(); if (vc.cachedVideo) vc.setVolume(clamp(vc.cachedVideo.volume + (e.deltaY < 0 ? 0.05 : -0.05), 0, 1)); }; Helpers.bindLongPress(el.bFav, () => setTimeout(() => this.bookmarks.open(), 50), () => { const name = currentSite.getName(); if (name && !isFavName(name)) { this.bookmarks.add(); this.ui.flash(el.bFav, 'OK'); this.updateViewersCount(); } else this.ui.flash(el.bFav, 'ERR'); } ); el.bFav.onclick = e => { e.preventDefault(); e.stopPropagation(); }; } restoreUiState() { this.ui.updateLabels(); this.ui.injectSiteCSS(); if (AppState.bgPlay) this.ui.enforceBackgroundPlay(); if (AppState.isMinimized) this.ui.toggleMinimize(true, true); if (AppState.hasPos) this.ui.clampPosition(); if (AppState.isHidden) this.ui.toggleHide(true); setTimeout(() => this.ui.ensureVisible(), 1500); setTimeout(() => this.ui.ensureVisible(), 5000); try { if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('Сбросить панель / Reset panel', () => this.ui.resetPanel()); } catch (e) { dbg(e, 'App.restoreUiState'); } try { if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand(pdT('title'), () => ParserReport.open()); } catch (e) { dbg(e, 'App.restoreUiState'); } let evT = 0; const later = () => { clearTimeout(evT); evT = setTimeout(() => this.ui.ensureVisible(), 700); }; ['resize', 'orientationchange'].forEach(t => window.addEventListener(t, later, { passive: true })); ['fullscreenchange', 'webkitfullscreenchange'].forEach(t => document.addEventListener(t, later, true)); setTimeout(() => this.multiView?.bootResume(), 2500); } bindRemoteAndWindow(vc) { onRemoteChange('bc_bookmarks', v => this.bookmarks.applyRemote(v)); onRemoteChange('bc_gmute', v => { try { AppState.gmute = JSON.parse(v) === true; } catch (e) { dbg(e, 'App.bindRemoteAndWindow'); } const cv = vc.cachedVideo; if (AppState.gmute && cv) { cv.muted = true; cv.volume = 0; } vc.syncUI(); }); window.addEventListener('resize', () => { if (AppState.hasPos) this.ui.clampPosition(); }, { passive: true }); onPageEvent('visibilitychange', () => { if (realHidden() || !AppState.hasPos) return; setTimeout(() => { this.ui.applyPosition(); this.ui.clampPosition(); }, 150); }); } startTimers() { let tick = 0; setInterval(() => { if (realHidden()) return; tick++; this.ui.injectSiteCSS(); this.updateViewersCount(); if (tick % 5 === 0) this.slowTasks(); }, 3000); setTimeout(() => this.updateViewersCount(), 1000); } slowTasks() { try { applyDark(); } catch (e) { dbg(e, 'App.slowTasks'); } this.ui.ensureVisible(); } updateViewersCount() { const name = currentSite.getName(); const { stats, bFav } = this.ui.elements; if (stats) stats.style.visibility = name ? 'visible' : 'hidden'; markFav(bFav, !!name && isFavName(name)); if (!name) return; const txt = stats && readViewers(); if (txt) paintViewers(stats, txt); const vn = parseCount(txt); if (Number.isFinite(vn)) this.bookmarks.setViewers(pageKey(), vn); } } const CBHP = { api: null }; const cbHoverPreview = () => { 'use strict'; if (document.documentElement.dataset.cbhp && document.getElementById('cbhp-css')) return; document.documentElement.dataset.cbhp = '1'; const MODES = ['off', 'anim', 'video']; const MODE_LABEL = { off: 'выключен', anim: 'только анимация обложки', video: 'анимация + мини-видео' }; const HEIGHTS = [240, 360, 480, 720]; const cfg = { mode: MODES.includes(Store.get('cbhp_mode', 'video')) ? Store.get('cbhp_mode', 'video') : 'video', sound: Store.get('cbhp_sound', false) === true, maxH: HEIGHTS.includes(+Store.get('cbhp_maxh', 360)) ? +Store.get('cbhp_maxh', 360) : 360 }; const T = { animDelay: 140, videoDelay: 320, frameMs: 50, sameMs: 250, okTtl: 180000, badTtl: 45000 }; const CARD = '.RoomCard, li.roomCard, li.room_list_room'; const IMG = 'img.RoomCardThumbnail__image, img[data-testid="room-card-image"], img.room_thumbnail'; const SKIP = '[class*="thumbnail_label_c_private"], [class*="thumbnail_label_offline"], [class*="thumbnail_label_c_away"], [class*="thumbnail_label_c_hidden"]'; const NAME_RE = /^[A-Za-z0-9_]{2,40}$/; const userOf = card => { let u = card.querySelector('a[data-room]')?.dataset.room; if (!u) { const href = card.querySelector('a[href]')?.getAttribute('href') || ''; u = (/^(?:https?:\/\/[^/]+)?\/([A-Za-z0-9_]+)\/?(?:[?#].*)?$/.exec(href) || [])[1]; } return u && NAME_RE.test(u) ? u : ''; }; const css = ` .cbhp-layer{position:absolute;inset:0;z-index:0;overflow:hidden;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .18s ease} .cbhp-layer.cbhp-in{opacity:1} .cbhp-f,.cbhp-v{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;display:block;margin:0;padding:0;border:0;background:none} .cbhp-f.cbhp-on{opacity:1} .cbhp-layer.cbhp-live .cbhp-v{opacity:1;transition:opacity .35s ease} .cbhp-bar{position:absolute;left:0;bottom:0;height:2px;width:34%;opacity:0;background:linear-gradient(90deg,transparent,#f47321,transparent)} .cbhp-load .cbhp-bar{display:none} .cbhp-dot{position:absolute;top:6px;right:6px;width:8px;height:8px;border-radius:50%;background:#ff3b30;box-shadow:0 0 0 0 rgba(255,59,48,.6);opacity:0;transition:opacity .25s} .cbhp-dot{display:none} .RoomCard .bcu-star,.roomCard .bcu-star,.RoomCard .RoomCardThumbnail__labelContainer,.RoomCard .RoomCard__followStar,.RoomCard .RoomCardThumbnail__followStar,.RoomCard .RoomCardThumbnail__topBar,.RoomCard .RoomCardThumbnail__bottomBar{z-index:2147483000!important;transform:translateZ(0)} .RoomCard .RoomCardThumbnail{isolation:isolate} @keyframes cbhp-slide{from{transform:translateX(-100%)}to{transform:translateX(300%)}} @keyframes cbhp-pulse{0%{box-shadow:0 0 0 0 rgba(255,59,48,.55)}100%{box-shadow:0 0 0 8px rgba(255,59,48,0)}} .cbhp-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483000;padding:8px 14px;border-radius:8px;background:rgba(20,20,22,.92);color:#fff;font:13px/1.3 system-ui,sans-serif;pointer-events:none;opacity:0;transition:opacity .2s} .cbhp-toast.cbhp-show{opacity:1} @media (hover:none){.RoomCard,.RoomCard a,.RoomCard img{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;-webkit-user-drag:none}} @media (prefers-reduced-motion:reduce){.cbhp-f,.cbhp-v,.cbhp-layer{transition:none}.cbhp-load .cbhp-bar,.cbhp-live .cbhp-dot{animation:none}} `; try { addStyle('cbhp-css', css, document.documentElement); } catch (e) { dbg(e, 'cbHoverPreview'); } const touchy = !matchMedia('(hover:hover)').matches; const DEBUG = Store.get('cbhp_debug', false) === true; const log = (...a) => { if (DEBUG) { try { console.log('[cbhp]', ...a); } catch (e) { dbg(e, 'cbHoverPreview.log'); } } }; log('init ok, mode=' + cfg.mode + ', GM_xhr=' + (typeof GM_xmlhttpRequest) + ', Hls=' + (typeof Hls)); const el = (tag, cls) => { const n = document.createElement(tag); if (cls) n.className = cls; return n; }; let toastEl = null, toastTimer = 0; const toast = text => { if (!toastEl) { toastEl = el('div', 'cbhp-toast'); document.body.append(toastEl); } toastEl.textContent = text; toastEl.classList.add('cbhp-show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove('cbhp-show'), 1800); }; const reg = (id, caption, fn) => { if (typeof GM_registerMenuCommand !== 'function') return; try { GM_registerMenuCommand(caption, fn, { id }); } catch { try { GM_registerMenuCommand(caption, fn); } catch (e) { dbg(e, 'cbHoverPreview.reg'); } } }; const setMode = m => { if (!MODES.includes(m)) return; cfg.mode = m; Store.set('cbhp_mode', m); stop(); buildMenu(); }; const setSound = on => { cfg.sound = on === true; Store.set('cbhp_sound', cfg.sound); stop(); buildMenu(); }; const setMaxH = h => { if (!HEIGHTS.includes(+h)) return; cfg.maxH = +h; Store.set('cbhp_maxh', cfg.maxH); stop(); buildMenu(); }; const buildMenu = () => { reg('mode', `Превью при наведении: ${MODE_LABEL[cfg.mode]}`, () => { setMode(MODES[(MODES.indexOf(cfg.mode) + 1) % MODES.length]); toast(`Превью: ${MODE_LABEL[cfg.mode]}`); }); reg('sound', `Звук при наведении: ${cfg.sound ? 'вкл' : 'выкл'}`, () => { setSound(!cfg.sound); toast(`Звук превью: ${cfg.sound ? 'вкл' : 'выкл'}`); }); reg('q', `Качество мини-видео: до ${cfg.maxH}p`, () => { setMaxH(HEIGHTS[(HEIGHTS.indexOf(cfg.maxH) + 1) % HEIGHTS.length]); toast(`Мини-видео: до ${cfg.maxH}p`); }); reg('dbg', `Отладка в консоли: ${DEBUG ? 'вкл' : 'выкл'} (нужна перезагрузка)`, () => { Store.set('cbhp_debug', !DEBUG); toast(`Отладка: ${!DEBUG ? 'вкл' : 'выкл'}, перезагрузите страницу`); }); }; buildMenu(); CBHP.api = { modes: MODES, heights: HEIGHTS, cfg, setMode, setSound, setMaxH }; const streams = new Map(); const pending = new Map(); const getStream = user => { const c = streams.get(user); if (c && Date.now() - c.t < (c.url ? T.okTtl : T.badTtl)) return Promise.resolve(c.url); if (pending.has(user)) return pending.get(user); const p = (async () => { try { const r = await fetch(`/api/chatvideocontext/${encodeURIComponent(user)}/`, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); log('chatvideocontext', user, r.status); if (!r.ok) return ''; const j = await r.json(); log('ctx', user, j && j.room_status, !!(j && j.hls_source)); const url = j && j.room_status === 'public' && typeof j.hls_source === 'string' && /^https:\/\//.test(j.hls_source) ? j.hls_source : ''; streams.set(user, { t: Date.now(), url }); if (streams.size > 300) streams.delete(streams.keys().next().value); return url; } catch (err) { log('ctx error', err && err.message); return ''; } finally { pending.delete(user); } })(); pending.set(user, p); return p; }; const forget = user => streams.delete(user); const frameUrls = u => [ `https://thumb.live.mmcdn.com/minifwap/${u}.jpg?${Date.now()}`, `https://jpeg.live.mmcdn.com/stream?room=${u}&f=${Math.random()}` ]; let needGM = false; const gmBlob = url => new Promise((res, rej) => { if (typeof GM_xmlhttpRequest !== 'function') return rej(new Error('no gm')); GM_xmlhttpRequest({ method: 'GET', url, responseType: 'blob', timeout: 8000, onload: r => (r.status >= 200 && r.status < 300 && r.response && r.response.size > 500) ? res({ url: URL.createObjectURL(r.response), size: r.response.size }) : rej(new Error('bad')), onerror: () => rej(new Error('err')), ontimeout: () => rej(new Error('timeout')) }); }); const OFFLINE_SIZES = new Set([4824, 4456, 9505, 2688, 3189, 4411, 3847]); const decodeInto = async (img, src) => { img.src = src; await img.decode(); }; async function pushFrame(s) { const next = s.frames[s.flip ^ 1], prev = s.frames[s.flip]; for (const url of frameUrls(s.user)) { if (s.dead) return false; try { if (needGM) { const b = await gmBlob(url); if (s.dead) { URL.revokeObjectURL(b.url); return false; } const old = next.dataset.blob; if (OFFLINE_SIZES.has(b.size)) { URL.revokeObjectURL(b.url); return 'same'; } if (b.size === s.lastSize) { URL.revokeObjectURL(b.url); return 'same'; } s.lastSize = b.size; try { await decodeInto(next, b.url); } catch (e) { URL.revokeObjectURL(b.url); throw e; } next.dataset.blob = b.url; if (old) URL.revokeObjectURL(old); } else { await decodeInto(next, url); } if (s.dead) return false; next.classList.add('cbhp-on'); prev.classList.remove('cbhp-on'); s.flip ^= 1; s.layer.classList.add('cbhp-in'); return 'new'; } catch (err) { log('frame fail', url, needGM ? 'gm' : 'direct', err && (err.message || err.name)); if (!needGM && typeof GM_xmlhttpRequest === 'function') { try { const b = await gmBlob(url); URL.revokeObjectURL(b.url); needGM = true; } catch (e) { dbg(e, 'cbHoverPreview.pushFrame'); } } } } return false; } async function animLoop(s) { let fails = 0; while (!s.dead && !s.live) { if (!s.card.isConnected) { stop(); return; } const t0 = performance.now(); const ok = await pushFrame(s); if (s.dead || s.live) return; fails = ok ? 0 : fails + 1; if (fails >= 3) return; await s.sleep(Math.max(ok === 'same' ? T.sameMs : T.frameMs - (performance.now() - t0), 0)); } } function pickLevel(levels) { let idx = -1, best = -1; levels.forEach((l, i) => { const h = l.height || 0; if (h && h <= cfg.maxH && h > best) { best = h; idx = i; } }); if (idx >= 0) return idx; let low = 0; levels.forEach((l, i) => { if ((l.height || 1e9) < (levels[low].height || 1e9)) low = i; }); return low; } const canHls = () => typeof Hls !== 'undefined' && Hls.isSupported(); function prepare(s) { getStream(s.user).then(url => { if (s.dead || !url || s.video || !canHls()) return; attach(s, url); }); } async function videoStart(s) { if (s.dead) return; s.go = true; s.layer.classList.add('cbhp-load'); if (s.hls && s.parsed) { s.parsed = false; s.hls.startLoad(-1); tryPlay(s.video, s); return; } if (s.video) return; const url = await getStream(s.user); if (s.dead || s.video) return; if (!url) { s.layer.classList.remove('cbhp-load'); return; } attach(s, url); } const tryPlay = (v, s) => { const pr = v.play(); if (pr && pr.catch) pr.catch(err => { log('play rejected', err && err.name, 'muted=' + v.muted); if (!v.muted && !s.dead) { v.muted = true; v.play().catch(() => {}); } }); }; function attach(s, url) { if (s.video || s.hls) return; s.parsed = false; try { previewDirs.add(hlsDir(url)); if (previewDirs.size > 40) previewDirs.delete(previewDirs.values().next().value); } catch (e) { dbg(e, 'cbHoverPreview.attach'); } const v = s.video = el('video', 'cbhp-v'); v.muted = !(cfg.sound && navigator.userActivation && navigator.userActivation.hasBeenActive); log('video attach, sound=' + cfg.sound + ', activation=' + !!(navigator.userActivation && navigator.userActivation.hasBeenActive) + ', muted=' + v.muted); v.playsInline = true; v.disablePictureInPicture = true; v.disableRemotePlayback = true; v.preload = 'auto'; s.layer.insertBefore(v, s.layer.querySelector('.cbhp-bar')); const ready = () => { if (s.dead || s.live) return; s.live = true; s.layer.classList.remove('cbhp-load'); s.layer.classList.add('cbhp-in', 'cbhp-live'); }; if ('requestVideoFrameCallback' in v) v.requestVideoFrameCallback(ready); else v.addEventListener('playing', ready, { once: true }); const fail = () => { if (!s.dead) { s.layer.classList.remove('cbhp-load'); killVideo(s); } }; if (canHls()) { const hls = s.hls = new Hls({ autoStartLoad: false, lowLatencyMode: false, enableWorker: true, backBufferLength: 0, maxBufferLength: 4, maxMaxBufferLength: 8, maxBufferSize: 8 * 1000 * 1000, liveSyncDurationCount: 2, manifestLoadingMaxRetry: 1, levelLoadingMaxRetry: 1, fragLoadingMaxRetry: 2 }); hls.on(Hls.Events.MANIFEST_PARSED, (_, d) => { if (s.dead || s.hls !== hls) return; const idx = pickLevel(d.levels); hls.startLevel = idx; hls.currentLevel = idx; if (!s.go) { s.parsed = true; return; } hls.startLoad(-1); tryPlay(v, s); }); hls.on(Hls.Events.ERROR, (_, d) => { log('hls error', d.type, d.details, d.fatal); if (!d.fatal || s.dead) return; if (!s.retried && d.type === Hls.ErrorTypes.NETWORK_ERROR) { s.retried = true; forget(s.user); killVideo(s); getStream(s.user).then(u => { if (!s.dead && u) attach(s, u); else fail(); }); } else fail(); }); hls.loadSource(url); hls.attachMedia(v); } else if (v.canPlayType('application/vnd.apple.mpegurl')) { v.src = url; tryPlay(v, s); v.addEventListener('error', fail, { once: true }); } else fail(); } function killVideo(s) { if (s.hls) { try { s.hls.stopLoad(); s.hls.detachMedia(); s.hls.destroy(); } catch (e) { dbg(e, 'cbHoverPreview.killVideo'); } s.hls = null; } if (s.video) { try { s.video.pause(); s.video.removeAttribute('src'); s.video.load(); } catch (e) { dbg(e, 'cbHoverPreview.killVideo'); } s.video.remove(); s.video = null; } } let cur = null; function start(card) { stop(); if (cfg.mode === 'off' || card.querySelector(SKIP)) return; const img = card.querySelector(IMG), user = userOf(card); log('start', { user, img: !!img, skip: !!card.querySelector(SKIP) }); if (!img || !user || !img.parentElement) return; const host = img.parentElement; const s = cur = { card, img, host, user, dead: false, live: false, flip: 0, retried: false, cancel: [], layer: null, frames: [], video: null, hls: null, restorePos: false, sleep(ms) { return new Promise(res => { const t = setTimeout(res, ms); s.cancel.push(() => { clearTimeout(t); res(); }); }); }, after(ms, fn) { const t = setTimeout(() => { if (!s.dead) fn(); }, ms); s.cancel.push(() => clearTimeout(t)); } }; s.after(touchy ? 0 : T.animDelay, () => { if (getComputedStyle(host).position === 'static') { host.style.position = 'relative'; s.restorePos = true; } const layer = s.layer = el('div', 'cbhp-layer'); const a = el('img', 'cbhp-f'), b = el('img', 'cbhp-f'); a.alt = b.alt = ''; a.decoding = b.decoding = 'async'; s.frames = [a, b]; layer.append(a, b, el('div', 'cbhp-bar'), el('div', 'cbhp-dot')); img.after(layer); log('layer added for', s.user); animLoop(s); if (cfg.mode === 'video') prepare(s); if (cfg.mode === 'video') s.after(touchy ? 150 : Math.max(0, T.videoDelay - T.animDelay), () => videoStart(s)); }); } function stop() { const s = cur; if (!s) return; cur = null; s.dead = true; s.cancel.forEach(fn => { try { fn(); } catch (e) { dbg(e, 'cbHoverPreview.stop'); } }); killVideo(s); const layer = s.layer; if (layer) { layer.classList.remove('cbhp-in'); setTimeout(() => { layer.remove(); s.frames.forEach(f => { if (f.dataset.blob) URL.revokeObjectURL(f.dataset.blob); f.removeAttribute('src'); }); if (s.restorePos) s.host.style.position = ''; }, 220); } } const onOver = e => { log('event', e.type, e.pointerType || '', e.target && e.target.className); if (cfg.mode === 'off' || e.buttons) return; const card = e.target.closest && e.target.closest(CARD); if (!card || (cur && cur.card === card)) return; if (e.relatedTarget && card.contains(e.relatedTarget)) return; start(card); }; const onOut = e => { if (!cur) return; if (e.pointerType === 'touch') return; const card = e.target.closest && e.target.closest(CARD); if (!card || card !== cur.card) return; if (e.relatedTarget && card.contains(e.relatedTarget)) return; stop(); }; ['pointerover', 'mouseover'].forEach(t => document.addEventListener(t, onOver, true)); ['pointerout', 'mouseout'].forEach(t => document.addEventListener(t, onOut, true)); document.addEventListener('contextmenu', e => { if (touchy && cfg.mode !== 'off' && e.target.closest && e.target.closest(CARD)) e.preventDefault(); }, true); document.addEventListener('click', e => { if (!touchy && cur && e.target.closest && e.target.closest(CARD)) stop(); }, true); let scrollTimer = 0; window.addEventListener('scroll', () => { if (!cur) return; clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { if (cur && (touchy || !cur.card.matches(':hover'))) stop(); }, 120); }, { passive: true, capture: true }); window.addEventListener('blur', stop); document.addEventListener('visibilitychange', () => { if (document.hidden) stop(); }); }; const i18n = { RU: { ago: "{n} назад", bg: "ФОН", bk_csv: "Таблицы CSV", bk_csv_fav: "Избранное", bk_csv_hist: "История", bk_csv_ses: "Сеансы", bk_exp: "Экспорт резервной копии", bk_imp: "Импорт резервной копии", bk_p_fav: "Избранное", bk_p_hist: "История", bk_p_set: "Настройки", bk_parts: "Состав копии", bm_add: "В избранное", bm_all: "Все", bm_del: "Удалить", bm_dup: "Уже добавлено!", bm_empty: "Список пуст.", bm_find: "Поиск по имени…", bm_nomatch: "Нет моделей по выбранному фильтру", bm_online: "Онлайн", bm_online_t: "Показывать только Онлайн", bm_pin: "Закрепить наверху", bm_priv: "Приват", bm_title: "Избранное", bm_unpin: "Открепить", bt_hints: "Подсказки", bt_stats: "Зрители", bt_vol: "Громкость", cal_all_tag: "Все теги", cal_allday: "Весь день", cal_c_dur: "Длит.", cal_c_model: "Модель", cal_c_n: "Заходы", cal_c_time: "Вход", cal_hist: "Просмотры", cal_loaded: "Расписание загружено: {a} из {b}", cal_models: "Модели", cal_no_sched: "Расписание берётся из профилей избранных. Откройте «Избранное» и подождите несколько секунд.", cal_none: "В этот день пусто", cal_reset: "Сбросить", cal_sched: "Расписание", cal_title: "Календарь", cal_today: "Сегодня", cal_total: "Итого", cal_watched: "Просмотрено", cf_cancel: "Отмена", cf_ok: "Да", ch_avg: "В среднем в день", ch_btn: "Диаграммы", ch_days: "Время по дням, 14 дней", ch_hours: "Активность по часам", ch_peak: "Пик", ch_top: "Топ по времени", copy: "ССЫЛКА", dark: "Тёмная / светлая тема", don_copied: "Адрес скопирован", don_fail: "Не удалось скопировать", don_sub: "Спасибо, что пользуешься", don_tap: "Нажмите на адрес, чтобы раскрыть его целиком. Кнопка справа копирует.", don_tip: "Поддержать автора", don_title: "Угостить автора кофе", drag: "⁞⁞", err: "ОШИБКА", favs: "ИЗБРАННОЕ", fr_hold: "Удерживайте — показать текущий кадр", fr_none: "Кадр недоступен: модель не в эфире", fs: "ЭКРАН", grp_all: "Все группы", grp_new: "+ Новая группа…", grp_none: "Без группы", hd_all: "Скрыть всё", hd_cats: "Категории", hd_clear: "Удалить все мои правила", hd_details: "Подробности", hd_g_chat: "Чат", hd_g_site: "Сайт", hd_g_win: "Окна", hd_hidden: "Скрыто {n} из {m}", hd_na: "пока не найдено", hd_none: "Правил пока нет.", hd_off: "Показать всё", hd_p_clean: "Чистый плеер", hd_p_min: "Минимум", hd_peek: "Удерживайте пункт, чтобы увидеть, что он скрывает", hd_pick: "Выбрать элемент", hd_reset: "Сбросить для этого сайта", hd_rules: "Мои правила", hd_title: "Скрытие", hide: "Скрыть (H)", hints: "M=ЗВУК F=ЭКРАН T=ТЕАТР H=СКРЫТЬ", hist_clear: "Очистить", hist_clear_q: "Удалить всю историю просмотров?", hist_del: "Убрать из истории", hist_empty: "Пока пусто.", hist_s_last: "Недавние", hist_s_time: "Время", hist_s_vis: "Визиты", hist_title: "История", hist_total: "Моделей: {n} · Всего: {t}", hist_visits: "виз.", home: "Домой", lang_auto: "Авто", lang_title: "Язык", min: "Свернуть", mv_abr_row: "Автокачество", mv_abr_t: "Автокачество: снижает качество при зависаниях или слабой сети и повышает, когда соединение ровное", mv_add: "Добавить", mv_auto: "Авто", mv_auto_all: "Добавлять все стримы сразу", mv_back: "Назад", mv_bg: "Играть в фоне", mv_bg_t: "Потоки продолжают играть при переключении вкладок, закрытии окна и выходе из браузера", mv_bm_h: "Закладки в эфире", mv_clear_q: "Закрыть все трансляции?", mv_close: "Закрыть", mv_cols: "Сетка", mv_cur: "Текущая трансляция", mv_dvr: "Буфер перемотки", mv_dvr_off: "Выкл", mv_fs: "Во весь экран", mv_full: "Окно заполнено", mv_go: "Добавить", mv_hint: "Добавьте трансляции, чтобы смотреть несколько сразу. Нажмите на плитку, чтобы включить на ней звук.", mv_link_h: "Или по ссылке", mv_live: "LIVE", mv_load: "Загрузка…", mv_min_u: "мин", mv_mute: "Выключить звук", mv_none: "Нет закладок в эфире", mv_off: "Трансляция недоступна", mv_pip: "Картинка в картинке", mv_play: "Пуск / пауза", mv_qual: "Качество", mv_retry: "нажмите, чтобы повторить", mv_search: "Поиск по онлайн-закладкам", mv_title: "Плеер", mv_url: "https://…/playlist.m3u8", no_vid: "НЕТ ВИДЕО", note_ph: "Заметка... (#теги)", pip: "PIP", pk_cancel: "Отмена", pk_found: "совп.", pk_hint: "Коснитесь элемента на странице", pk_ok: "Скрыть", pk_up: "Выше", qual: "КАЧ-ВО", rad_media: "Медиа", rad_nav: "Избранное", rad_next: "След.", rad_page: "Страница", rad_prev: "Пред.", rad_reset: "Сбросить позицию кружка", rad_title: "Круговое меню", rad_vdn: "Тише", rad_view: "Экран", rad_vup: "Громче", rec: "ЗАПИСЬ", rec_tip: "Нажатие — запись, удержание — настройки", rel: "Рестарт", rs_cur: "Сейчас поток:", rs_gbh: "ГБ/ч", rs_hint: "Когда часть достигает выбранного размера, она сохраняется отдельным файлом, а запись продолжается в новой. Данные пишутся на диск небольшими порциями; если диск недоступен, они остаются в памяти телефона, и тогда лучше выбрать меньшие части.", rs_mbmin: "МБ/мин", rs_next: "Изменения вступают в силу со следующей части или новой записи.", rs_now: "Сейчас", rs_off: "Выкл.", rs_p: "часть", rs_part: "Размер части (файл делится)", rs_q_hi: "Высокое", rs_q_low: "Низкое", rs_q_mid: "Среднее", rs_q_min: "Экономия", rs_quality: "Качество (битрейт)", rs_recover: "Восстановить несохранённые записи", rs_resnote: "Захват пишет в разрешении потока: настройки выше меняют только битрейт (сжатие), а 1080p–360p показывают ожидаемое качество картинки. В режиме HLS выбирается поток не выше указанной высоты.", rs_src: "Источник", rs_src_cap: "Захват (перекодирование)", rs_src_hls: "HLS (без перекодирования)", rs_src_note: "HLS скачивает сегменты потока как есть, без перекодирования, и сохраняет MP4. Нужен адрес потока; если его нет, используется захват. Качество оригинальное, как у исходного потока. Запись пишется на диск небольшими порциями.", rs_t: "всего", rs_title: "Настройки записи", rs_total: "Остановить при общем размере", rs_upto: "до", search: "ПОИСК", seen_now: "только что", seen_tip: "Последний раз онлайн", settings: "Настройки", sleep_custom: "Своё время", sleep_hint: "Через выбранное время плеер будет выключен, а запись остановится и сохранится. Нажмите на чёрный экран, чтобы вернуть плеер.", sleep_left: "Осталось", sleep_sec: "сек", sleep_start: "Запустить", sleep_title: "Таймер сна", sleep_wake: "Таймер сна сработал.\nНажмите, чтобы продолжить", snap: "СКРИН", sort_date: "Дата", sort_last: "Был(а)", sort_live: "Лайв", sort_name: "Имя", sort_viewers: "Зрители", sound: "ЗВУК", st_av_refresh: "Обновить аватарки", st_away: "BRB", st_back: "Назад", st_btns: "Кнопки панели", st_btns_hint: "Нажмите, чтобы скрыть кнопку или вернуть её на панель.", st_btns_reset: "Показать все кнопки", st_color: "Цвет панели", st_color_rnd: "Случайный", st_cols: "Колонки", st_custom: "Свой цвет", st_data: "Данные", st_fav: "Избранное", st_favnav: "Кнопки «назад / вперёд» на пульте", st_group: "Группа", st_hide_grp: "Скрыть выбор группы", st_hide_note: "Скрыть поле заметки (теги)", st_hide_topic: "Скрыть описание модели", st_hist: "История просмотров", st_hist_on: "Вести историю", st_hue: "Оттенок", st_lang: "Язык", st_lig: "Яркость", st_list: "Список", st_look: "Оформление", st_open: "Открыть", st_prev: "Превью моделей", st_prev_off: "Не обновлять", st_prev_spd: "Обновление превью", st_private: "Приват", st_ptags: "Скрыть теги профиля (BongaCams)", st_rec: "Запись видео", st_sat: "Насыщенность", st_scale: "Размер панели", st_site_dark: "Тёмная тема сайта", st_star: "Звёздочка на миниатюрах", st_vip: "VIP", star_add: "В избранное", star_del: "Убрать из избранного", star_del_q: "У этой модели есть заметка или группа. Убрать из избранного?", thtr: "ТЕАТР", tp_less: "[Свернуть]", tp_more: "[Ещё]", u_d: "д", u_gb: "ГБ", u_h: "ч", u_m: "мин", u_mb: "МБ", viewers_tip: "Зрителей в комнате", w_new: "Не просмотрено", w_seen: "Просмотрено", }, EN: { ago: "{n} ago", bg: "BG", bk_csv: "CSV tables", bk_csv_fav: "Favorites", bk_csv_hist: "History", bk_csv_ses: "Sessions", bk_exp: "Export backup", bk_imp: "Import backup", bk_p_fav: "Favorites", bk_p_hist: "History", bk_p_set: "Settings", bk_parts: "Backup contents", bm_add: "Add to favorites", bm_all: "All", bm_del: "Delete", bm_dup: "Already added!", bm_empty: "List is empty.", bm_find: "Search by name…", bm_nomatch: "No models match filter", bm_online: "Online", bm_online_t: "Show only Online", bm_pin: "Pin to top", bm_priv: "Priv", bm_title: "Favorites", bm_unpin: "Unpin", bt_hints: "Hints", bt_stats: "Viewers", bt_vol: "Volume", cal_all_tag: "All tags", cal_allday: "All day", cal_c_dur: "Duration", cal_c_model: "Model", cal_c_n: "Visits", cal_c_time: "Joined", cal_hist: "History", cal_loaded: "Schedule loaded: {a} of {b}", cal_models: "Models", cal_no_sched: "The schedule is read from the profiles of your favorites. Open Favorites and wait a few seconds.", cal_none: "Nothing on this day", cal_reset: "Reset", cal_sched: "Schedule", cal_title: "Calendar", cal_today: "Today", cal_total: "Total", cal_watched: "Viewed", cf_cancel: "Cancel", cf_ok: "Yes", ch_avg: "Daily average", ch_btn: "Charts", ch_days: "Time per day, 14 days", ch_hours: "Activity by hour", ch_peak: "Peak", ch_top: "Top by time", copy: "LINK", dark: "Dark / light theme", don_copied: "Address copied", don_fail: "Copy failed", don_sub: "Thanks for using it", don_tap: "Tap an address to expand it. The button on the right copies it.", don_tip: "Support the author", don_title: "Buy the author a coffee", drag: "⁞⁞", err: "ERROR", favs: "FAVS", fr_hold: "Hold to show the current frame", fr_none: "No frame: model is offline", fs: "FULL", grp_all: "All groups", grp_new: "+ New group…", grp_none: "No group", hd_all: "Hide all", hd_cats: "Categories", hd_clear: "Delete all my rules", hd_details: "Details", hd_g_chat: "Chat", hd_g_site: "Site", hd_g_win: "Pop-ups", hd_hidden: "Hidden {n} of {m}", hd_na: "not found yet", hd_none: "No rules yet.", hd_off: "Show all", hd_p_clean: "Clean player", hd_p_min: "Minimal", hd_peek: "Hold an item to see what it hides", hd_pick: "Pick element", hd_reset: "Reset for this site", hd_rules: "My rules", hd_title: "Hiding", hide: "Hide (H)", hints: "M=MUTE F=FULL T=THTR H=HIDE", hist_clear: "Clear", hist_clear_q: "Delete the whole watch history?", hist_del: "Remove from history", hist_empty: "Empty for now.", hist_s_last: "Recent", hist_s_time: "Time", hist_s_vis: "Visits", hist_title: "History", hist_total: "Models: {n} · Total: {t}", hist_visits: "visits", home: "Home", lang_auto: "Auto", lang_title: "Language", min: "Minimize", mv_abr_row: "Auto quality", mv_abr_t: "Auto quality: lowers the quality when the video stutters or the network gets weak, raises it when the connection is stable", mv_add: "Add", mv_auto: "Auto", mv_auto_all: "Add all live streams automatically", mv_back: "Back", mv_bg: "Play in background", mv_bg_t: "Streams keep playing when you switch tabs, close this window or leave the browser", mv_bm_h: "Live bookmarks", mv_clear_q: "Close all streams?", mv_close: "Close", mv_cols: "Layout", mv_cur: "Current stream", mv_dvr: "Rewind buffer", mv_dvr_off: "Off", mv_fs: "Fullscreen", mv_full: "Window is full", mv_go: "Add", mv_hint: "Add streams to watch several at once. Tap a tile to move the sound to it.", mv_link_h: "Or by link", mv_live: "LIVE", mv_load: "Loading…", mv_min_u: "min", mv_mute: "Mute", mv_none: "No live bookmarks", mv_off: "Stream unavailable", mv_pip: "Picture-in-picture", mv_play: "Play / pause", mv_qual: "Quality", mv_retry: "tap to retry", mv_search: "Search live bookmarks", mv_title: "Player", mv_url: "https://…/playlist.m3u8", no_vid: "NO VIDEO", note_ph: "Note... (#tags)", pip: "PIP", pk_cancel: "Cancel", pk_found: "matches", pk_hint: "Tap an element on the page", pk_ok: "Hide", pk_up: "Up", qual: "QUAL", rad_media: "Media", rad_nav: "Favorites", rad_next: "Next", rad_page: "Page", rad_prev: "Prev", rad_reset: "Reset button position", rad_title: "Radial menu", rad_vdn: "Quieter", rad_view: "View", rad_vup: "Louder", rec: "REC", rec_tip: "Tap: record · Hold: settings", rel: "Reload", rs_cur: "Current stream:", rs_gbh: "GB/h", rs_hint: "When a part reaches the chosen size, it is saved as a separate file and recording continues in a new one. Data is written to disk in small chunks; if the disk is unavailable it stays in phone memory, so choose smaller parts.", rs_mbmin: "MB/min", rs_next: "Changes apply from the next part or a new recording.", rs_now: "Now", rs_off: "Off", rs_p: "part", rs_part: "Part size (file is split)", rs_q_hi: "High", rs_q_low: "Low", rs_q_mid: "Medium", rs_q_min: "Saver", rs_quality: "Quality (bitrate)", rs_recover: "Recover unsaved recordings", rs_resnote: "Capture records at the stream resolution: the options above only change the bitrate (compression), and 1080p–360p show the expected picture quality. In HLS mode, a stream no taller than the chosen height is picked.", rs_src: "Source", rs_src_cap: "Capture (re-encode)", rs_src_hls: "HLS (no re-encode)", rs_src_note: "HLS downloads the stream segments as they are, without re-encoding, and saves MP4. It needs the stream address; if it is not available, Capture is used instead. Quality is original, as in the source stream. Recording is written to disk in small checkpoints.", rs_t: "total", rs_title: "Recording settings", rs_total: "Stop at total size", rs_upto: "up to", search: "SEARCH", seen_now: "just now", seen_tip: "Last seen online", settings: "Settings", sleep_custom: "Custom time", sleep_hint: "When time is up, the player is switched off and any recording stops and is saved. Tap the black screen to bring it back.", sleep_left: "Left", sleep_sec: "sec", sleep_start: "Start", sleep_title: "Sleep timer", sleep_wake: "Sleep timer finished.\nTap to continue", snap: "SNAP", sort_date: "Date", sort_last: "Seen", sort_live: "Live", sort_name: "Name", sort_viewers: "Viewers", sound: "VOL", st_av_refresh: "Refresh avatars", st_away: "BRB", st_back: "Back", st_btns: "Panel buttons", st_btns_hint: "Tap to hide a button or bring it back.", st_btns_reset: "Show all buttons", st_color: "Panel color", st_color_rnd: "Random", st_cols: "Columns", st_custom: "Custom color", st_data: "Data", st_fav: "Favorites", st_favnav: "Prev / next buttons on remote", st_group: "Group", st_hide_grp: "Hide group selector", st_hide_note: "Hide note field (tags)", st_hide_topic: "Hide model description", st_hist: "Watch history", st_hist_on: "Keep history", st_hue: "Hue", st_lang: "Language", st_lig: "Lightness", st_list: "List", st_look: "Appearance", st_open: "Open", st_prev: "Model previews", st_prev_off: "No refresh", st_prev_spd: "Preview refresh", st_private: "Private", st_ptags: "Hide profile tags (BongaCams)", st_rec: "Video recording", st_sat: "Saturation", st_scale: "Panel size", st_site_dark: "Site dark theme", st_star: "Star on thumbnails", st_vip: "VIP", star_add: "Add to favorites", star_del: "Remove from favorites", star_del_q: "This model has a note or a group. Remove from favorites?", thtr: "THTR", tp_less: "[Less]", tp_more: "[More]", u_d: "d", u_gb: "GB", u_h: "h", u_m: "min", u_mb: "MB", viewers_tip: "Viewers in the room", w_new: "Not watched", w_seen: "Watched", }, UK: { ago: "{n} тому", bg: "ФОН", bk_csv: "Таблиці CSV", bk_csv_fav: "Обране", bk_csv_hist: "Історія", bk_csv_ses: "Сеанси", bk_exp: "Експорт резервної копії", bk_imp: "Імпорт резервної копії", bk_p_fav: "Обране", bk_p_hist: "Історія", bk_p_set: "Налаштування", bk_parts: "Склад копії", bm_add: "В обране", bm_all: "Усі", bm_del: "Видалити", bm_dup: "Вже додано!", bm_empty: "Список порожній.", bm_find: "Пошук за іменем…", bm_nomatch: "Немає моделей за вибраним фільтром", bm_online: "Онлайн", bm_online_t: "Показувати лише Онлайн", bm_pin: "Закріпити нагорі", bm_priv: "Приват", bm_title: "Обране", bm_unpin: "Відкріпити", bt_hints: "Підказки", bt_stats: "Глядачі", bt_vol: "Гучність", cal_all_tag: "Усі теги", cal_allday: "Увесь день", cal_c_dur: "Трив.", cal_c_model: "Модель", cal_c_n: "Заходи", cal_c_time: "Вхід", cal_hist: "Перегляди", cal_loaded: "Розклад завантажено: {a} з {b}", cal_models: "Моделі", cal_no_sched: "Розклад береться з профілів обраних. Відкрийте «Обране» і зачекайте кілька секунд.", cal_none: "У цей день порожньо", cal_reset: "Скинути", cal_sched: "Розклад", cal_title: "Календар", cal_today: "Сьогодні", cal_total: "Разом", cal_watched: "Переглянуто", cf_cancel: "Скасувати", cf_ok: "Так", ch_avg: "У середньому за день", ch_btn: "Діаграми", ch_days: "Час по днях, 14 днів", ch_hours: "Активність по годинах", ch_peak: "Пік", ch_top: "Топ за часом", copy: "ЛІНК", dark: "Темна / світла тема", don_copied: "Адресу скопійовано", don_fail: "Не вдалося скопіювати", don_sub: "Дякую, що користуєшся", don_tap: "Натисни на адресу, щоб розгорнути її повністю. Кнопка праворуч копіює її.", don_tip: "Підтримати автора", don_title: "Пригостити автора кавою", drag: "⁞⁞ ПУЛЬТ", err: "ПОМИЛКА", favs: "ОБРАНЕ", fr_hold: "Утримуйте — показати поточний кадр", fr_none: "Кадр недоступний: модель не в ефірі", fs: "ЕКРАН", grp_all: "Усі групи", grp_new: "+ Нова група…", grp_none: "Без групи", hd_all: "Сховати все", hd_cats: "Категорії", hd_clear: "Видалити всі мої правила", hd_details: "Подробиці", hd_g_chat: "Чат", hd_g_site: "Сайт", hd_g_win: "Вікна", hd_hidden: "Приховано {n} з {m}", hd_na: "поки не знайдено", hd_none: "Правил поки немає.", hd_off: "Показати все", hd_p_clean: "Чистий плеєр", hd_p_min: "Мінімум", hd_peek: "Утримуйте пункт, щоб побачити, що він ховає", hd_pick: "Обрати елемент", hd_reset: "Скинути для цього сайту", hd_rules: "Мої правила", hd_title: "Приховування", hide: "Сховати (H)", hints: "M=ЗВУК F=ЕКРАН T=ТЕАТР H=СХОВАТИ", hist_clear: "Очистити", hist_clear_q: "Видалити всю історію переглядів?", hist_del: "Прибрати з історії", hist_empty: "Поки порожньо.", hist_s_last: "Нещодавні", hist_s_time: "Час", hist_s_vis: "Візити", hist_title: "Історія", hist_total: "Моделей: {n} · Усього: {t}", hist_visits: "віз.", home: "Додому", lang_auto: "Авто", lang_title: "Мова", min: "Згорнути", mv_abr_row: "Автоякість", mv_abr_t: "Автоякість: знижує якість при підвисаннях або слабкій мережі та підвищує, коли з’єднання стабільне", mv_add: "Додати", mv_auto: "Авто", mv_auto_all: "Додавати всі стріми одразу", mv_back: "Назад", mv_bg: "Грати у фоні", mv_bg_t: "Потоки продовжують грати, коли ви перемикаєте вкладки, закриваєте вікно або виходите з браузера", mv_bm_h: "Закладки в ефірі", mv_clear_q: "Закрити всі трансляції?", mv_close: "Закрити", mv_cols: "Сітка", mv_cur: "Поточна трансляція", mv_dvr: "Буфер перемотування", mv_dvr_off: "Вимк.", mv_fs: "На весь екран", mv_full: "Вікно заповнене", mv_go: "Додати", mv_hint: "Додайте трансляції, щоб дивитися кілька одразу. Торкніться плитки, щоб увімкнути на ній звук.", mv_link_h: "Або за посиланням", mv_load: "Завантаження…", mv_min_u: "хв", mv_mute: "Вимкнути звук", mv_none: "Немає закладок в ефірі", mv_off: "Трансляція недоступна", mv_pip: "Картинка в картинці", mv_play: "Пуск / пауза", mv_qual: "Якість", mv_retry: "торкніться, щоб повторити", mv_search: "Пошук за закладками в ефірі", mv_title: "Плеєр", no_vid: "НЕМАЄ ВІДЕО", note_ph: "Нотатка... (#теги)", pip: "PIP", pk_cancel: "Скасувати", pk_found: "збіг.", pk_hint: "Торкніться елемента на сторінці", pk_ok: "Сховати", pk_up: "Вище", qual: "ЯКІСТЬ", rad_media: "Медіа", rad_nav: "Обране", rad_next: "Наст.", rad_page: "Сторінка", rad_prev: "Попер.", rad_reset: "Скинути позицію кружечка", rad_title: "Колове меню", rad_vdn: "Тихіше", rad_view: "Екран", rad_vup: "Гучніше", rec: "ЗАПИС", rec_tip: "Дотик — запис, утримання — налаштування", rel: "Перезапуск", rs_cur: "Зараз потік:", rs_gbh: "ГБ/год", rs_hint: "Коли частина досягає вибраного розміру, вона зберігається окремим файлом, а запис триває в новій. Дані пишуться на диск невеликими порціями; якщо диск недоступний, вони лишаються в памʼяті телефона, і тоді краще обрати менші частини.", rs_mbmin: "МБ/хв", rs_next: "Зміни діють з наступної частини або нового запису.", rs_now: "Зараз", rs_off: "Вимк.", rs_p: "частина", rs_part: "Розмір частини (файл ділиться)", rs_q_hi: "Висока", rs_q_low: "Низька", rs_q_mid: "Середня", rs_q_min: "Економія", rs_quality: "Якість (бітрейт)", rs_recover: "Відновити незбережені записи", rs_resnote: "Захоплення пише в роздільній здатності потоку: налаштування вище змінюють лише бітрейт (стиснення), а 1080p–360p показують очікувану якість картинки. У режимі HLS обирається потік не вищий за вказану висоту.", rs_src: "Джерело", rs_src_cap: "Захоплення (перекодування)", rs_src_hls: "HLS (без перекодування)", rs_src_note: "HLS завантажує сегменти потоку як є, без перекодування, і зберігає MP4. Потрібна адреса потоку; якщо її немає, використовується захоплення. Якість оригінальна, як у вихідного потоку. Запис пишеться на диск невеликими порціями.", rs_t: "всього", rs_title: "Налаштування запису", rs_total: "Зупинити при загальному розмірі", rs_upto: "до", search: "ПОШУК", seen_now: "щойно", seen_tip: "Востаннє онлайн", settings: "Налаштування", sleep_custom: "Свій час", sleep_hint: "Через обраний час плеєр буде вимкнено, а запис зупиниться й збережеться. Натисніть на чорний екран, щоб повернути плеєр.", sleep_left: "Залишилось", sleep_sec: "сек", sleep_start: "Запустити", sleep_title: "Таймер сну", sleep_wake: "Таймер сну спрацював.\nНатисніть, щоб продовжити", snap: "СКРІН", sort_date: "Дата", sort_last: "Був(ла)", sort_live: "Лайв", sort_name: "Імʼя", sort_viewers: "Глядачі", sound: "ЗВУК", st_av_refresh: "Оновити аватарки", st_away: "BRB", st_back: "Назад", st_btns: "Кнопки панелі", st_btns_hint: "Натисніть, щоб сховати кнопку або повернути її на панель.", st_btns_reset: "Показати всі кнопки", st_color: "Колір панелі", st_color_rnd: "Випадковий", st_cols: "Колонки", st_custom: "Свій колір", st_data: "Дані", st_fav: "Обране", st_favnav: "Кнопки «назад / вперед» на пульті", st_group: "Група", st_hide_grp: "Сховати вибір групи", st_hide_note: "Сховати поле нотатки (теги)", st_hide_topic: "Сховати опис моделі", st_hist: "Історія переглядів", st_hist_on: "Зберігати історію", st_hue: "Відтінок", st_lang: "Мова", st_lig: "Яскравість", st_list: "Список", st_look: "Оформлення", st_open: "Відкрити", st_prev: "Превʼю моделей", st_prev_off: "Не оновлювати", st_prev_spd: "Оновлення превʼю", st_private: "Приват", st_ptags: "Сховати теги профілю (BongaCams)", st_rec: "Запис відео", st_sat: "Насиченість", st_scale: "Розмір панелі", st_site_dark: "Темна тема сайту", st_star: "Зірочка на мініатюрах", st_vip: "VIP", star_add: "В обране", star_del: "Прибрати з обраного", star_del_q: "У цієї моделі є нотатка або група. Прибрати з обраного?", thtr: "ТЕАТР", tp_less: "[Згорнути]", tp_more: "[Ще]", u_d: "д", u_gb: "ГБ", u_h: "год", u_m: "хв", u_mb: "МБ", viewers_tip: "Глядачів у кімнаті", w_new: "Не переглянуто", w_seen: "Переглянуто", }, ES: { ago: "hace {n}", bg: "FONDO", bk_csv: "Tablas CSV", bk_csv_fav: "Favoritos", bk_csv_hist: "Historial", bk_csv_ses: "Sesiones", bk_exp: "Exportar copia de seguridad", bk_imp: "Importar copia de seguridad", bk_p_fav: "Favoritos", bk_p_hist: "Historial", bk_p_set: "Ajustes", bk_parts: "Contenido de la copia", bm_add: "Añadir a favoritos", bm_all: "Todos", bm_del: "Eliminar", bm_dup: "¡Ya añadida!", bm_empty: "La lista está vacía.", bm_find: "Buscar por nombre…", bm_nomatch: "Ninguna modelo coincide con el filtro", bm_online: "Online", bm_online_t: "Mostrar solo online", bm_pin: "Fijar arriba", bm_priv: "Priv", bm_title: "Favoritos", bm_unpin: "Desfijar", bt_hints: "Consejos", bt_stats: "Espectadores", bt_vol: "Volumen", cal_all_tag: "Todas las etiquetas", cal_allday: "Todo el día", cal_c_dur: "Dur.", cal_c_model: "Modelo", cal_c_n: "Visitas", cal_c_time: "Entrada", cal_hist: "Vistos", cal_loaded: "Horario cargado: {a} de {b}", cal_models: "Modelos", cal_no_sched: "El horario se toma de los perfiles de tus favoritos. Abre Favoritos y espera unos segundos.", cal_none: "No hay nada este día", cal_reset: "Restablecer", cal_sched: "Horario", cal_title: "Calendario", cal_today: "Hoy", cal_total: "Total", cal_watched: "Visto", cf_cancel: "Cancelar", cf_ok: "Sí", ch_avg: "Media diaria", ch_btn: "Gráficos", ch_days: "Tiempo por día, 14 días", ch_hours: "Actividad por hora", ch_peak: "Pico", ch_top: "Top por tiempo", copy: "ENLACE", dark: "Tema oscuro / claro", don_copied: "Dirección copiada", don_fail: "No se pudo copiar", don_sub: "Gracias por usarlo", don_tap: "Toca una dirección para verla completa. El botón de la derecha la copia.", don_tip: "Apoyar al autor", don_title: "Invita al autor a un café", drag: "⁞⁞ MANDO", err: "ERROR", favs: "FAVS", fr_hold: "Mantén pulsado para ver el fotograma actual", fr_none: "Sin imagen: la modelo no está en directo", fs: "PANT.", grp_all: "Todos los grupos", grp_new: "+ Nuevo grupo…", grp_none: "Sin grupo", hd_all: "Ocultar todo", hd_cats: "Categorías", hd_clear: "Borrar todas mis reglas", hd_details: "Detalles", hd_g_chat: "Chat", hd_g_site: "Sitio", hd_g_win: "Ventanas emergentes", hd_hidden: "Ocultos {n} de {m}", hd_na: "aún no encontrado", hd_none: "Aún no hay reglas.", hd_off: "Mostrar todo", hd_p_clean: "Reproductor limpio", hd_p_min: "Mínimo", hd_peek: "Mantén pulsado un elemento para ver qué oculta", hd_pick: "Elegir elemento", hd_reset: "Restablecer para este sitio", hd_rules: "Mis reglas", hd_title: "Ocultar", hide: "Ocultar (H)", hints: "M=SONIDO F=PANT T=CINE H=OCULTAR", hist_clear: "Borrar", hist_clear_q: "¿Borrar todo el historial de visualización?", hist_del: "Quitar del historial", hist_empty: "Vacío por ahora. El tiempo se cuenta mientras se reproduce el vídeo.", hist_s_last: "Recientes", hist_s_time: "Tiempo", hist_s_vis: "Visitas", hist_title: "Historial", hist_total: "Modelos: {n} · Total: {t}", hist_visits: "visitas", home: "Inicio", lang_auto: "Automático", lang_title: "Idioma", min: "Minimizar", mv_abr_row: "Calidad automática", mv_abr_t: "Calidad automática: baja la calidad cuando el vídeo se corta o la red es débil y la sube cuando la conexión es estable", mv_add: "Añadir", mv_auto: "Auto", mv_auto_all: "Añadir todas las transmisiones en directo automáticamente", mv_back: "Atrás", mv_bg: "Reproducir en segundo plano", mv_bg_t: "Las transmisiones siguen reproduciéndose al cambiar de pestaña, cerrar esta ventana o salir del navegador", mv_bm_h: "Favoritos en directo", mv_clear_q: "¿Cerrar todas las transmisiones?", mv_close: "Cerrar", mv_cols: "Cuadrícula", mv_cur: "Transmisión actual", mv_dvr: "Búfer de retroceso", mv_dvr_off: "Desactivado", mv_fs: "Pantalla completa", mv_full: "La ventana está llena", mv_go: "Añadir", mv_hint: "Añade transmisiones para ver varias a la vez. Toca un recuadro para pasarle el sonido.", mv_link_h: "O mediante enlace", mv_load: "Cargando…", mv_min_u: "min", mv_mute: "Silenciar", mv_none: "No hay favoritos en directo", mv_off: "Transmisión no disponible", mv_pip: "Imagen en imagen", mv_play: "Reproducir / pausa", mv_qual: "Calidad", mv_retry: "toca para reintentar", mv_search: "Buscar favoritos en directo", mv_title: "Reproductor", no_vid: "SIN VÍDEO", note_ph: "Nota... (#etiquetas)", pip: "PIP", pk_cancel: "Cancelar", pk_found: "coinc.", pk_hint: "Toca un elemento de la página", pk_ok: "Ocultar", pk_up: "Subir", qual: "CALID.", rad_media: "Medios", rad_nav: "Favoritos", rad_next: "Sig.", rad_page: "Página", rad_prev: "Ant.", rad_reset: "Restablecer posición del botón", rad_title: "Menú radial", rad_vdn: "Más bajo", rad_view: "Pantalla", rad_vup: "Más alto", rec: "GRABAR", rec_tip: "Toque: grabar · Mantener: ajustes", rel: "Recargar", rs_cur: "Transmisión actual:", rs_gbh: "GB/h", rs_hint: "Cuando una parte alcanza el tamaño elegido, se guarda como archivo aparte y la grabación continúa en uno nuevo. Los datos se escriben en el disco en pequeños bloques; si el disco no está disponible, permanecen en la memoria del teléfono, así que conviene elegir partes más pequeñas.", rs_mbmin: "MB/min", rs_next: "Los cambios se aplican desde la siguiente parte o una grabación nueva.", rs_now: "Ahora", rs_off: "Desactivado", rs_p: "parte", rs_part: "Tamaño de parte (divide el archivo)", rs_q_hi: "Alta", rs_q_low: "Baja", rs_q_mid: "Media", rs_q_min: "Ahorro", rs_quality: "Calidad (bitrate)", rs_recover: "Recuperar grabaciones sin guardar", rs_resnote: "La captura graba con la resolución de la transmisión: las opciones de arriba solo cambian el bitrate (compresión), y 1080p–360p indican la calidad de imagen esperada. En el modo HLS se elige una transmisión que no supere la altura indicada.", rs_src: "Origen", rs_src_cap: "Captura (recodificación)", rs_src_hls: "HLS (sin recodificar)", rs_src_note: "HLS descarga los segmentos de la transmisión tal cual, sin recodificar, y guarda MP4. Necesita la dirección de la transmisión; si no está disponible, se usa la captura. La calidad es la original, la del flujo de origen. La grabación se escribe en disco en pequeños puntos de control.", rs_t: "total", rs_title: "Ajustes de grabación", rs_total: "Detener al alcanzar el tamaño total", rs_upto: "hasta", search: "BUSCAR", seen_now: "ahora mismo", seen_tip: "Última vez online", settings: "Ajustes", sleep_custom: "Tiempo personalizado", sleep_hint: "Al terminar el tiempo, el reproductor se apaga y cualquier grabación se detiene y se guarda. Toca la pantalla negra para recuperarlo.", sleep_left: "Quedan", sleep_sec: "seg", sleep_start: "Iniciar", sleep_title: "Temporizador de apagado", sleep_wake: "El temporizador ha terminado.\nToca para continuar", snap: "CAPT.", sort_date: "Fecha", sort_last: "Visto", sort_live: "En vivo", sort_name: "Nombre", sort_viewers: "Espectadores", sound: "SONIDO", st_av_refresh: "Actualizar avatares", st_away: "BRB", st_back: "Atrás", st_btns: "Botones del panel", st_btns_hint: "Toca para ocultar un botón o devolverlo.", st_btns_reset: "Mostrar todos los botones", st_color: "Color del panel", st_color_rnd: "Aleatorio", st_cols: "Columnas", st_custom: "Color personalizado", st_data: "Datos", st_fav: "Favoritos", st_favnav: "Botones «anterior / siguiente» en el mando", st_group: "Grupo", st_hide_grp: "Ocultar el selector de grupo", st_hide_note: "Ocultar campo de nota (etiquetas)", st_hide_topic: "Ocultar la descripción de la modelo", st_hist: "Historial de visualización", st_hist_on: "Guardar historial", st_hue: "Tono", st_lang: "Idioma", st_lig: "Luminosidad", st_list: "Lista", st_look: "Apariencia", st_open: "Abrir", st_prev: "Vistas previas de modelos", st_prev_off: "Sin actualizar", st_prev_spd: "Actualización de la vista previa", st_private: "Privado", st_ptags: "Ocultar etiquetas del perfil (BongaCams)", st_rec: "Grabación de vídeo", st_sat: "Saturación", st_scale: "Tamaño del panel", st_site_dark: "Tema oscuro del sitio", st_star: "Estrella en las miniaturas", st_vip: "VIP", star_add: "Añadir a favoritos", star_del: "Quitar de favoritos", star_del_q: "Esta modelo tiene una nota o un grupo. ¿Quitar de favoritos?", thtr: "CINE", tp_less: "[Menos]", tp_more: "[Más]", u_d: "d", u_gb: "GB", u_h: "h", u_m: "min", u_mb: "MB", viewers_tip: "Espectadores en la sala", w_new: "No visto", w_seen: "Visto", }, DE: { ago: "vor {n}", bg: "HINT.", bk_csv: "CSV-Tabellen", bk_csv_fav: "Favoriten", bk_csv_hist: "Verlauf", bk_csv_ses: "Sitzungen", bk_exp: "Sicherung exportieren", bk_imp: "Sicherung importieren", bk_p_fav: "Favoriten", bk_p_hist: "Verlauf", bk_p_set: "Einstellungen", bk_parts: "Inhalt der Sicherung", bm_add: "Zu Favoriten hinzufügen", bm_all: "Alle", bm_del: "Löschen", bm_dup: "Schon hinzugefügt!", bm_empty: "Die Liste ist leer.", bm_find: "Nach Namen suchen…", bm_nomatch: "Keine Models passen zum Filter", bm_online: "Online", bm_online_t: "Nur Online anzeigen", bm_pin: "Oben anheften", bm_priv: "Priv", bm_title: "Favoriten", bm_unpin: "Anheften aufheben", bt_hints: "Hinweise", bt_stats: "Zuschauer", bt_vol: "Lautstärke", cal_all_tag: "Alle Tags", cal_allday: "Ganztägig", cal_c_dur: "Dauer", cal_c_model: "Model", cal_c_n: "Besuche", cal_c_time: "Eintritt", cal_hist: "Verlauf", cal_loaded: "Zeitplan geladen: {a} von {b}", cal_models: "Models", cal_no_sched: "Der Zeitplan stammt aus den Profilen deiner Favoriten. Öffne die Favoriten und warte ein paar Sekunden.", cal_none: "An diesem Tag ist nichts", cal_reset: "Zurücksetzen", cal_sched: "Zeitplan", cal_title: "Kalender", cal_today: "Heute", cal_total: "Gesamt", cal_watched: "Gesehen", cf_cancel: "Abbrechen", cf_ok: "Ja", ch_avg: "Tagesdurchschnitt", ch_btn: "Diagramme", ch_days: "Zeit pro Tag, 14 Tage", ch_hours: "Aktivität nach Stunde", ch_peak: "Spitze", ch_top: "Top nach Zeit", copy: "LINK", dark: "Dunkles / helles Design", don_copied: "Adresse kopiert", don_fail: "Kopieren fehlgeschlagen", don_sub: "Danke fürs Benutzen", don_tap: "Tippe auf eine Adresse, um sie komplett zu sehen. Die Schaltfläche rechts kopiert sie.", don_tip: "Autor unterstützen", don_title: "Spendiere dem Autor einen Kaffee", drag: "⁞⁞ STEUERUNG", err: "FEHLER", favs: "FAVS", fr_hold: "Gedrückt halten für das aktuelle Bild", fr_none: "Kein Bild: Model ist offline", fs: "VOLL", grp_all: "Alle Gruppen", grp_new: "+ Neue Gruppe…", grp_none: "Keine Gruppe", hd_all: "Alles ausblenden", hd_cats: "Kategorien", hd_clear: "Alle meine Regeln löschen", hd_details: "Details", hd_g_chat: "Chat", hd_g_site: "Seite", hd_g_win: "Pop-ups", hd_hidden: "{n} von {m} ausgeblendet", hd_na: "noch nicht gefunden", hd_none: "Noch keine Regeln.", hd_off: "Alles zeigen", hd_p_clean: "Sauberer Player", hd_p_min: "Minimal", hd_peek: "Eintrag gedrückt halten, um zu sehen, was er ausblendet", hd_pick: "Element wählen", hd_reset: "Für diese Seite zurücksetzen", hd_rules: "Meine Regeln", hd_title: "Ausblenden", hide: "Ausblenden (H)", hints: "M=TON F=VOLL T=KINO H=AUS", hist_clear: "Leeren", hist_clear_q: "Gesamten Wiedergabeverlauf löschen?", hist_del: "Aus dem Verlauf entfernen", hist_empty: "Noch leer. Die Zeit wird gezählt, solange das Video läuft.", hist_s_last: "Zuletzt", hist_s_time: "Zeit", hist_s_vis: "Besuche", hist_title: "Verlauf", hist_total: "Models: {n} · Gesamt: {t}", hist_visits: "Besuche", home: "Start", lang_auto: "Automatisch", lang_title: "Sprache", min: "Minimieren", mv_abr_row: "Automatische Qualität", mv_abr_t: "Automatische Qualität: senkt die Qualität bei Rucklern oder schwachem Netz und erhöht sie, wenn die Verbindung stabil ist", mv_add: "Hinzufügen", mv_auto: "Auto", mv_auto_all: "Alle Live-Streams automatisch hinzufügen", mv_back: "Zurück", mv_bg: "Im Hintergrund abspielen", mv_bg_t: "Streams laufen weiter, wenn du den Tab wechselst, dieses Fenster schließt oder den Browser verlässt", mv_bm_h: "Live-Lesezeichen", mv_clear_q: "Alle Streams schließen?", mv_close: "Schließen", mv_cols: "Layout", mv_cur: "Aktueller Stream", mv_dvr: "Zurückspul-Puffer", mv_dvr_off: "Aus", mv_fs: "Vollbild", mv_full: "Fenster ist voll", mv_go: "Hinzufügen", mv_hint: "Füge Streams hinzu, um mehrere gleichzeitig zu sehen. Tippe auf eine Kachel, um den Ton dorthin zu legen.", mv_link_h: "Oder per Link", mv_load: "Wird geladen…", mv_min_u: "Min", mv_mute: "Stumm", mv_none: "Keine Live-Lesezeichen", mv_off: "Stream nicht verfügbar", mv_pip: "Bild-in-Bild", mv_play: "Wiedergabe / Pause", mv_qual: "Qualität", mv_retry: "zum Wiederholen tippen", mv_search: "Live-Lesezeichen durchsuchen", mv_title: "Player", no_vid: "KEIN VIDEO", note_ph: "Notiz... (#Tags)", pip: "PIP", pk_cancel: "Abbrechen", pk_found: "Treffer", pk_hint: "Tippe auf ein Element der Seite", pk_ok: "Ausblenden", pk_up: "Höher", qual: "QUAL.", rad_media: "Medien", rad_nav: "Favoriten", rad_next: "Weiter", rad_page: "Seite", rad_prev: "Zurück", rad_reset: "Position des Buttons zurücksetzen", rad_title: "Radialmenü", rad_vdn: "Leiser", rad_view: "Ansicht", rad_vup: "Lauter", rec: "AUFN.", rec_tip: "Tippen: aufnehmen · Halten: Einstellungen", rel: "Neu laden", rs_cur: "Aktueller Stream:", rs_gbh: "GB/Std", rs_hint: "Erreicht ein Teil die gewählte Größe, wird er als eigene Datei gespeichert und die Aufnahme läuft in einem neuen weiter. Die Daten werden in kleinen Blöcken auf den Datenträger geschrieben; ist er nicht verfügbar, bleiben sie im Arbeitsspeicher des Handys – wähle dann kleinere Teile.", rs_mbmin: "MB/Min", rs_next: "Änderungen gelten ab dem nächsten Teil oder einer neuen Aufnahme.", rs_now: "Jetzt", rs_off: "Aus", rs_p: "Teil", rs_part: "Teilgröße (Datei wird geteilt)", rs_q_hi: "Hoch", rs_q_low: "Niedrig", rs_q_mid: "Mittel", rs_q_min: "Sparen", rs_quality: "Qualität (Bitrate)", rs_recover: "Nicht gespeicherte Aufnahmen wiederherstellen", rs_resnote: "Der Capture-Modus nimmt in der Stream-Auflösung auf: Die Optionen oben ändern nur die Bitrate (Kompression), und 1080p–360p zeigen die erwartete Bildqualität. Im HLS-Modus wird ein Stream gewählt, der nicht höher als die angegebene Höhe ist.", rs_src: "Quelle", rs_src_cap: "Mitschnitt (Neukodierung)", rs_src_hls: "HLS (ohne Neukodierung)", rs_src_note: "HLS lädt die Stream-Segmente unverändert ohne Neukodierung herunter und speichert MP4. Dafür wird die Stream-Adresse benötigt; ist sie nicht verfügbar, wird der Mitschnitt genutzt. Die Qualität entspricht dem Original-Stream. Die Aufzeichnung wird in kleinen Schritten auf die Festplatte geschrieben.", rs_t: "gesamt", rs_title: "Aufnahme-Einstellungen", rs_total: "Stopp bei Gesamtgröße", rs_upto: "bis zu", search: "SUCHE", seen_now: "gerade eben", seen_tip: "Zuletzt online", settings: "Einstellungen", sleep_custom: "Eigene Zeit", sleep_hint: "Nach Ablauf der Zeit wird der Player ausgeschaltet, und eine laufende Aufnahme wird gestoppt und gespeichert. Tippe auf den schwarzen Bildschirm, um ihn zurückzuholen.", sleep_left: "Verbleibend", sleep_sec: "Sek", sleep_start: "Starten", sleep_title: "Schlaf-Timer", sleep_wake: "Schlaf-Timer abgelaufen.\nTippen zum Fortsetzen", snap: "FOTO", sort_date: "Datum", sort_last: "Zuletzt", sort_live: "Live", sort_name: "Name", sort_viewers: "Zuschauer", sound: "TON", st_av_refresh: "Avatare aktualisieren", st_away: "BRB", st_back: "Zurück", st_btns: "Panel-Schaltflächen", st_btns_hint: "Tippen, um eine Schaltfläche aus- oder wieder einzublenden.", st_btns_reset: "Alle Schaltflächen anzeigen", st_color: "Panelfarbe", st_color_rnd: "Zufällig", st_cols: "Spalten", st_custom: "Eigene Farbe", st_data: "Daten", st_fav: "Favoriten", st_favnav: "Zurück-/Weiter-Tasten an der Steuerung", st_group: "Gruppe", st_hide_grp: "Gruppenauswahl ausblenden", st_hide_note: "Notizfeld (Tags) ausblenden", st_hide_topic: "Modellbeschreibung ausblenden", st_hist: "Wiedergabeverlauf", st_hist_on: "Verlauf speichern", st_hue: "Farbton", st_lang: "Sprache", st_lig: "Helligkeit", st_list: "Liste", st_look: "Darstellung", st_open: "Öffnen", st_prev: "Modell-Vorschauen", st_prev_off: "Keine Aktualisierung", st_prev_spd: "Vorschau-Aktualisierung", st_private: "Privat", st_ptags: "Profil-Tags ausblenden (BongaCams)", st_rec: "Videoaufnahme", st_sat: "Sättigung", st_scale: "Panelgröße", st_site_dark: "Dunkles Seitendesign", st_star: "Stern auf Vorschaubildern", st_vip: "VIP", star_add: "Zu Favoriten hinzufügen", star_del: "Aus Favoriten entfernen", star_del_q: "Dieses Model hat eine Notiz oder eine Gruppe. Aus Favoriten entfernen?", thtr: "KINO", tp_less: "[Weniger]", tp_more: "[Mehr]", u_d: "T", u_gb: "GB", u_h: "Std", u_m: "Min", u_mb: "MB", viewers_tip: "Zuschauer im Raum", w_new: "Nicht gesehen", w_seen: "Gesehen", }, FR: { ago: "il y a {n}", bg: "FOND", bk_csv: "Tableaux CSV", bk_csv_fav: "Favoris", bk_csv_hist: "Historique", bk_csv_ses: "Sessions", bk_exp: "Exporter la sauvegarde", bk_imp: "Importer la sauvegarde", bk_p_fav: "Favoris", bk_p_hist: "Historique", bk_p_set: "Paramètres", bk_parts: "Contenu de la sauvegarde", bm_add: "Ajouter aux favoris", bm_all: "Tous", bm_del: "Supprimer", bm_dup: "Déjà ajouté !", bm_empty: "La liste est vide.", bm_find: "Rechercher par nom…", bm_nomatch: "Aucun modèle ne correspond au filtre", bm_online: "En ligne", bm_online_t: "Afficher seulement en ligne", bm_pin: "Épingler en haut", bm_priv: "Privé", bm_title: "Favoris", bm_unpin: "Désépingler", bt_hints: "Astuces", bt_stats: "Spectateurs", bt_vol: "Volume", cal_all_tag: "Tous les tags", cal_allday: "Toute la journée", cal_c_dur: "Durée", cal_c_model: "Modèle", cal_c_n: "Visites", cal_c_time: "Entrée", cal_hist: "Historique", cal_loaded: "Horaires chargés : {a} sur {b}", cal_models: "Modèles", cal_no_sched: "Les horaires sont lus dans les profils de vos favoris. Ouvrez les favoris et patientez quelques secondes.", cal_none: "Rien ce jour-là", cal_reset: "Réinitialiser", cal_sched: "Horaires", cal_title: "Calendrier", cal_today: "Aujourd'hui", cal_total: "Total", cal_watched: "Vu", cf_cancel: "Annuler", cf_ok: "Oui", ch_avg: "Moyenne quotidienne", ch_btn: "Graphiques", ch_days: "Temps par jour, 14 jours", ch_hours: "Activité par heure", ch_peak: "Pic", ch_top: "Top par durée", copy: "LIEN", dark: "Thème sombre / clair", don_copied: "Adresse copiée", don_fail: "Échec de la copie", don_sub: "Merci de l’utiliser", don_tap: "Touchez une adresse pour l’afficher en entier. Le bouton à droite la copie.", don_tip: "Soutenir l’auteur", don_title: "Offrir un café à l’auteur", drag: "⁞⁞ COMMANDE", err: "ERREUR", favs: "FAVORIS", fr_hold: "Maintenez pour voir l’image actuelle", fr_none: "Image indisponible : le modèle est hors ligne", fs: "PLEIN", grp_all: "Tous les groupes", grp_new: "+ Nouveau groupe…", grp_none: "Sans groupe", hd_all: "Tout masquer", hd_cats: "Catégories", hd_clear: "Supprimer toutes mes règles", hd_details: "Détails", hd_g_chat: "Chat", hd_g_site: "Site", hd_g_win: "Pop-ups", hd_hidden: "Masqués : {n} sur {m}", hd_na: "pas encore trouvé", hd_none: "Aucune règle pour l’instant.", hd_off: "Tout afficher", hd_p_clean: "Lecteur épuré", hd_p_min: "Minimal", hd_peek: "Maintenez un élément pour voir ce qu’il masque", hd_pick: "Choisir un élément", hd_reset: "Réinitialiser pour ce site", hd_rules: "Mes règles", hd_title: "Masquage", hide: "Masquer (H)", hints: "M=SON F=PLEIN T=CINÉ H=MASQUER", hist_clear: "Effacer", hist_clear_q: "Supprimer tout l’historique de visionnage ?", hist_del: "Retirer de l’historique", hist_empty: "Vide pour l’instant. Le temps est compté pendant la lecture de la vidéo.", hist_s_last: "Récents", hist_s_time: "Durée", hist_s_vis: "Visites", hist_title: "Historique", hist_total: "Modèles : {n} · Total : {t}", hist_visits: "visites", home: "Accueil", lang_auto: "Auto", lang_title: "Langue", min: "Réduire", mv_abr_row: "Qualité auto", mv_abr_t: "Qualité auto : réduit la qualité en cas de saccades ou de réseau faible et l’augmente quand la connexion est stable", mv_add: "Ajouter", mv_auto: "Auto", mv_auto_all: "Ajouter automatiquement tous les flux en direct", mv_back: "Retour", mv_bg: "Lecture en arrière-plan", mv_bg_t: "Les flux continuent de jouer quand vous changez d’onglet, fermez cette fenêtre ou quittez le navigateur", mv_bm_h: "Favoris en direct", mv_clear_q: "Fermer tous les flux ?", mv_close: "Fermer", mv_cols: "Disposition", mv_cur: "Flux actuel", mv_dvr: "Tampon de retour arrière", mv_dvr_off: "Désactivé", mv_fs: "Plein écran", mv_full: "La fenêtre est pleine", mv_go: "Ajouter", mv_hint: "Ajoutez des flux pour en regarder plusieurs à la fois. Touchez une vignette pour y basculer le son.", mv_link_h: "Ou par lien", mv_load: "Chargement…", mv_min_u: "min", mv_mute: "Couper le son", mv_none: "Aucun favori en direct", mv_off: "Flux indisponible", mv_pip: "Image dans l’image", mv_play: "Lecture / pause", mv_qual: "Qualité", mv_retry: "touchez pour réessayer", mv_search: "Rechercher dans les favoris en direct", mv_title: "Lecteur", no_vid: "PAS DE VIDÉO", note_ph: "Note... (#tags)", pip: "PIP", pk_cancel: "Annuler", pk_found: "corresp.", pk_hint: "Touchez un élément de la page", pk_ok: "Masquer", pk_up: "Plus haut", qual: "QUAL.", rad_media: "Médias", rad_nav: "Favoris", rad_next: "Suiv.", rad_page: "Page", rad_prev: "Préc.", rad_reset: "Réinitialiser la position du bouton", rad_title: "Menu radial", rad_vdn: "Moins fort", rad_view: "Affichage", rad_vup: "Plus fort", rec: "REC", rec_tip: "Toucher : enregistrer · Maintenir : réglages", rel: "Recharger", rs_cur: "Flux actuel :", rs_gbh: "Go/h", rs_hint: "Quand une partie atteint la taille choisie, elle est enregistrée dans un fichier distinct et l’enregistrement continue dans un nouveau. Les données sont écrites sur le disque par petits blocs ; si le disque est indisponible, elles restent dans la mémoire du téléphone, mieux vaut alors choisir des parties plus petites.", rs_mbmin: "Mo/min", rs_next: "Les modifications s’appliquent à partir de la partie suivante ou d’un nouvel enregistrement.", rs_now: "Maintenant", rs_off: "Désactivé", rs_p: "partie", rs_part: "Taille d’une partie (fichier découpé)", rs_q_hi: "Haute", rs_q_low: "Basse", rs_q_mid: "Moyenne", rs_q_min: "Éco", rs_quality: "Qualité (débit)", rs_recover: "Récupérer les enregistrements non sauvegardés", rs_resnote: "La capture enregistre à la résolution du flux : les options ci-dessus ne modifient que le débit (compression), et 1080p–360p indiquent la qualité d’image attendue. En mode HLS, un flux dont la hauteur ne dépasse pas celle choisie est sélectionné.", rs_src: "Source", rs_src_cap: "Capture (réencodage)", rs_src_hls: "HLS (sans réencodage)", rs_src_note: "HLS télécharge les segments du flux tels quels, sans réencodage, et enregistre en MP4. L’adresse du flux est nécessaire ; si elle est indisponible, la capture est utilisée. La qualité est celle d’origine, identique au flux source. L’enregistrement est écrit sur le disque par petits points de contrôle.", rs_t: "total", rs_title: "Réglages d’enregistrement", rs_total: "Arrêter à la taille totale", rs_upto: "jusqu’à", search: "RECH.", seen_now: "à l’instant", seen_tip: "Dernière connexion", settings: "Paramètres", sleep_custom: "Durée personnalisée", sleep_hint: "À la fin du temps, le lecteur s’éteint et tout enregistrement s’arrête et est sauvegardé. Touchez l’écran noir pour le rallumer.", sleep_left: "Restant", sleep_sec: "s", sleep_start: "Démarrer", sleep_title: "Minuteur de veille", sleep_wake: "Minuteur terminé.\nTouchez pour continuer", snap: "PHOTO", sort_date: "Date", sort_last: "Vu", sort_live: "En direct", sort_name: "Nom", sort_viewers: "Spectateurs", sound: "SON", st_av_refresh: "Actualiser les avatars", st_away: "BRB", st_back: "Retour", st_btns: "Boutons du panneau", st_btns_hint: "Touchez pour masquer un bouton ou le remettre.", st_btns_reset: "Afficher tous les boutons", st_color: "Couleur du panneau", st_color_rnd: "Aléatoire", st_cols: "Colonnes", st_custom: "Couleur personnalisée", st_data: "Données", st_fav: "Favoris", st_favnav: "Boutons « précédent / suivant » sur la télécommande", st_group: "Groupe", st_hide_grp: "Masquer le sélecteur de groupe", st_hide_note: "Masquer le champ de note (tags)", st_hide_topic: "Masquer la description du modèle", st_hist: "Historique de visionnage", st_hist_on: "Conserver l’historique", st_hue: "Teinte", st_lang: "Langue", st_lig: "Luminosité", st_list: "Liste", st_look: "Apparence", st_open: "Ouvrir", st_prev: "Aperçus des modèles", st_prev_off: "Pas d’actualisation", st_prev_spd: "Actualisation des aperçus", st_private: "Privé", st_ptags: "Masquer les tags du profil (BongaCams)", st_rec: "Enregistrement vidéo", st_sat: "Saturation", st_scale: "Taille du panneau", st_site_dark: "Thème sombre du site", st_star: "Étoile sur les miniatures", st_vip: "VIP", star_add: "Ajouter aux favoris", star_del: "Retirer des favoris", star_del_q: "Ce modèle a une note ou un groupe. Le retirer des favoris ?", thtr: "CINÉMA", tp_less: "[Moins]", tp_more: "[Plus]", u_d: "j", u_gb: "Go", u_h: "h", u_m: "min", u_mb: "Mo", viewers_tip: "Spectateurs dans le salon", w_new: "Non vu", w_seen: "Vu", }, PT: { ago: "há {n}", bg: "FUNDO", bk_csv: "Tabelas CSV", bk_csv_fav: "Favoritos", bk_csv_hist: "Histórico", bk_csv_ses: "Sessões", bk_exp: "Exportar backup", bk_imp: "Importar backup", bk_p_fav: "Favoritos", bk_p_hist: "Histórico", bk_p_set: "Configurações", bk_parts: "Conteúdo do backup", bm_add: "Adicionar aos favoritos", bm_all: "Todos", bm_del: "Excluir", bm_dup: "Já adicionada!", bm_empty: "A lista está vazia.", bm_find: "Buscar por nome…", bm_nomatch: "Nenhuma modelo corresponde ao filtro", bm_online: "Online", bm_online_t: "Mostrar só online", bm_pin: "Fixar no topo", bm_priv: "Priv", bm_title: "Favoritos", bm_unpin: "Desafixar", bt_hints: "Dicas", bt_stats: "Espectadores", bt_vol: "Volume", cal_all_tag: "Todas as tags", cal_allday: "O dia todo", cal_c_dur: "Dur.", cal_c_model: "Modelo", cal_c_n: "Visitas", cal_c_time: "Entrada", cal_hist: "Histórico", cal_loaded: "Horários carregados: {a} de {b}", cal_models: "Modelos", cal_no_sched: "Os horários vêm dos perfis dos seus favoritos. Abra os Favoritos e aguarde alguns segundos.", cal_none: "Nada neste dia", cal_reset: "Redefinir", cal_sched: "Horários", cal_title: "Calendário", cal_today: "Hoje", cal_total: "Total", cal_watched: "Visto", cf_cancel: "Cancelar", cf_ok: "Sim", ch_avg: "Média diária", ch_btn: "Gráficos", ch_days: "Tempo por dia, 14 dias", ch_hours: "Atividade por hora", ch_peak: "Pico", ch_top: "Top por tempo", copy: "LINK", dark: "Tema escuro / claro", don_copied: "Endereço copiado", don_fail: "Falha ao copiar", don_sub: "Obrigado por usar", don_tap: "Toque em um endereço para vê-lo completo. O botão à direita o copia.", don_tip: "Apoiar o autor", don_title: "Pague um café ao autor", drag: "⁞⁞ CONTROLE", err: "ERRO", favs: "FAVS", fr_hold: "Segure para ver o quadro atual", fr_none: "Sem imagem: a modelo está offline", fs: "TELA", grp_all: "Todos os grupos", grp_new: "+ Novo grupo…", grp_none: "Sem grupo", hd_all: "Ocultar tudo", hd_cats: "Categorias", hd_clear: "Apagar todas as minhas regras", hd_details: "Detalhes", hd_g_chat: "Chat", hd_g_site: "Site", hd_g_win: "Pop-ups", hd_hidden: "{n} de {m} ocultos", hd_na: "ainda não encontrado", hd_none: "Ainda sem regras.", hd_off: "Mostrar tudo", hd_p_clean: "Player limpo", hd_p_min: "Mínimo", hd_peek: "Segure um item para ver o que ele oculta", hd_pick: "Escolher elemento", hd_reset: "Redefinir para este site", hd_rules: "Minhas regras", hd_title: "Ocultar", hide: "Ocultar (H)", hints: "M=SOM F=TELA T=CINEMA H=OCULTAR", hist_clear: "Limpar", hist_clear_q: "Apagar todo o histórico de visualização?", hist_del: "Remover do histórico", hist_empty: "Vazio por enquanto. O tempo é contado enquanto o vídeo está tocando.", hist_s_last: "Recentes", hist_s_time: "Tempo", hist_s_vis: "Visitas", hist_title: "Histórico", hist_total: "Modelos: {n} · Total: {t}", hist_visits: "visitas", home: "Início", lang_auto: "Automático", lang_title: "Idioma", min: "Minimizar", mv_abr_row: "Qualidade automática", mv_abr_t: "Qualidade automática: reduz a qualidade quando o vídeo trava ou a rede está fraca e aumenta quando a conexão está estável", mv_add: "Adicionar", mv_auto: "Auto", mv_auto_all: "Adicionar todas as transmissões ao vivo automaticamente", mv_back: "Voltar", mv_bg: "Reproduzir em segundo plano", mv_bg_t: "As transmissões continuam tocando ao trocar de aba, fechar esta janela ou sair do navegador", mv_bm_h: "Favoritos ao vivo", mv_clear_q: "Fechar todas as transmissões?", mv_close: "Fechar", mv_cols: "Layout", mv_cur: "Transmissão atual", mv_dvr: "Buffer de retrocesso", mv_dvr_off: "Desativado", mv_fs: "Tela cheia", mv_full: "A janela está cheia", mv_go: "Adicionar", mv_hint: "Adicione transmissões para assistir a várias ao mesmo tempo. Toque em um bloco para passar o som para ele.", mv_link_h: "Ou por link", mv_load: "Carregando…", mv_min_u: "min", mv_mute: "Silenciar", mv_none: "Nenhum favorito ao vivo", mv_off: "Transmissão indisponível", mv_pip: "Imagem em imagem", mv_play: "Reproduzir / pausar", mv_qual: "Qualidade", mv_retry: "toque para tentar de novo", mv_search: "Buscar favoritos ao vivo", mv_title: "Player", no_vid: "SEM VÍDEO", note_ph: "Nota... (#tags)", pip: "PIP", pk_cancel: "Cancelar", pk_found: "result.", pk_hint: "Toque em um elemento da página", pk_ok: "Ocultar", pk_up: "Acima", qual: "QUAL.", rad_media: "Mídia", rad_nav: "Favoritos", rad_next: "Próx.", rad_page: "Página", rad_prev: "Ant.", rad_reset: "Redefinir posição do botão", rad_title: "Menu radial", rad_vdn: "Mais baixo", rad_view: "Tela", rad_vup: "Mais alto", rec: "GRAVAR", rec_tip: "Toque: gravar · Segurar: ajustes", rel: "Recarregar", rs_cur: "Transmissão atual:", rs_gbh: "GB/h", rs_hint: "Quando uma parte atinge o tamanho escolhido, ela é salva como um arquivo separado e a gravação continua em um novo. Os dados são gravados no disco em pequenos blocos; se o disco não estiver disponível, ficam na memória do celular, e então é melhor escolher partes menores.", rs_mbmin: "MB/min", rs_next: "As alterações valem a partir da próxima parte ou de uma nova gravação.", rs_now: "Agora", rs_off: "Desativado", rs_p: "parte", rs_part: "Tamanho da parte (divide o arquivo)", rs_q_hi: "Alta", rs_q_low: "Baixa", rs_q_mid: "Média", rs_q_min: "Economia", rs_quality: "Qualidade (bitrate)", rs_recover: "Recuperar gravações não salvas", rs_resnote: "A captura grava na resolução da transmissão: as opções acima alteram apenas o bitrate (compressão), e 1080p–360p indicam a qualidade de imagem esperada. No modo HLS, é escolhida uma transmissão com altura não superior à definida.", rs_src: "Origem", rs_src_cap: "Captura (recodificação)", rs_src_hls: "HLS (sem recodificação)", rs_src_note: "O HLS baixa os segmentos da transmissão como estão, sem recodificar, e salva em MP4. Precisa do endereço da transmissão; se não estiver disponível, a captura é usada. A qualidade é a original, igual à da transmissão de origem. A gravação é escrita no disco em pequenos pontos de verificação.", rs_t: "total", rs_title: "Ajustes de gravação", rs_total: "Parar ao atingir o tamanho total", rs_upto: "até", search: "BUSCAR", seen_now: "agora mesmo", seen_tip: "Última vez online", settings: "Configurações", sleep_custom: "Tempo personalizado", sleep_hint: "Ao fim do tempo, o player é desligado e qualquer gravação é interrompida e salva. Toque na tela preta para trazê-lo de volta.", sleep_left: "Restante", sleep_sec: "seg", sleep_start: "Iniciar", sleep_title: "Temporizador de sono", sleep_wake: "O temporizador terminou.\nToque para continuar", snap: "FOTO", sort_date: "Data", sort_last: "Visto", sort_live: "Ao vivo", sort_name: "Nome", sort_viewers: "Espectadores", sound: "SOM", st_av_refresh: "Atualizar avatares", st_away: "BRB", st_back: "Voltar", st_btns: "Botões do painel", st_btns_hint: "Toque para ocultar um botão ou trazê-lo de volta.", st_btns_reset: "Mostrar todos os botões", st_color: "Cor do painel", st_color_rnd: "Aleatória", st_cols: "Colunas", st_custom: "Cor personalizada", st_data: "Dados", st_fav: "Favoritos", st_favnav: "Botões «anterior / próximo» no controle", st_group: "Grupo", st_hide_grp: "Ocultar o seletor de grupo", st_hide_note: "Ocultar campo de nota (tags)", st_hide_topic: "Ocultar a descrição da modelo", st_hist: "Histórico de visualização", st_hist_on: "Manter histórico", st_hue: "Matiz", st_lang: "Idioma", st_lig: "Luminosidade", st_list: "Lista", st_look: "Aparência", st_open: "Abrir", st_prev: "Pré-visualizações das modelos", st_prev_off: "Sem atualização", st_prev_spd: "Atualização da pré-visualização", st_private: "Privado", st_ptags: "Ocultar tags do perfil (BongaCams)", st_rec: "Gravação de vídeo", st_sat: "Saturação", st_scale: "Tamanho do painel", st_site_dark: "Tema escuro do site", st_star: "Estrela nas miniaturas", st_vip: "VIP", star_add: "Adicionar aos favoritos", star_del: "Remover dos favoritos", star_del_q: "Esta modelo tem uma nota ou um grupo. Remover dos favoritos?", thtr: "CINEMA", tp_less: "[Menos]", tp_more: "[Mais]", u_d: "d", u_gb: "GB", u_h: "h", u_m: "min", u_mb: "MB", viewers_tip: "Espectadores na sala", w_new: "Não visto", w_seen: "Visto", }, IT: { ago: "{n} fa", bg: "SFONDO", bk_csv: "Tabelle CSV", bk_csv_fav: "Preferiti", bk_csv_hist: "Cronologia", bk_csv_ses: "Sessioni", bk_exp: "Esporta backup", bk_imp: "Importa backup", bk_p_fav: "Preferiti", bk_p_hist: "Cronologia", bk_p_set: "Impostazioni", bk_parts: "Contenuto del backup", bm_add: "Aggiungi ai preferiti", bm_all: "Tutti", bm_del: "Elimina", bm_dup: "Già aggiunta!", bm_empty: "L’elenco è vuoto.", bm_find: "Cerca per nome…", bm_nomatch: "Nessuna modella corrisponde al filtro", bm_online: "Online", bm_online_t: "Mostra solo online", bm_pin: "Fissa in alto", bm_priv: "Priv", bm_title: "Preferiti", bm_unpin: "Sblocca", bt_hints: "Suggerimenti", bt_stats: "Spettatori", bt_vol: "Volume", cal_all_tag: "Tutti i tag", cal_allday: "Tutto il giorno", cal_c_dur: "Durata", cal_c_model: "Modella", cal_c_n: "Visite", cal_c_time: "Ingresso", cal_hist: "Cronologia", cal_loaded: "Orari caricati: {a} su {b}", cal_models: "Modelle", cal_no_sched: "Gli orari vengono letti dai profili dei tuoi preferiti. Apri i Preferiti e attendi qualche secondo.", cal_none: "Niente in questo giorno", cal_reset: "Reimposta", cal_sched: "Orari", cal_title: "Calendario", cal_today: "Oggi", cal_total: "Totale", cal_watched: "Visto", cf_cancel: "Annulla", cf_ok: "Sì", ch_avg: "Media giornaliera", ch_btn: "Grafici", ch_days: "Tempo per giorno, 14 giorni", ch_hours: "Attività per ora", ch_peak: "Picco", ch_top: "Top per tempo", copy: "LINK", dark: "Tema scuro / chiaro", don_copied: "Indirizzo copiato", don_fail: "Copia non riuscita", don_sub: "Grazie per l’uso", don_tap: "Tocca un indirizzo per vederlo per intero. Il pulsante a destra lo copia.", don_tip: "Sostieni l’autore", don_title: "Offri un caffè all’autore", drag: "⁞⁞ TELECOMANDO", err: "ERRORE", favs: "PREF.", fr_hold: "Tieni premuto per vedere il fotogramma attuale", fr_none: "Nessun fotogramma: la modella è offline", fs: "SCHERMO", grp_all: "Tutti i gruppi", grp_new: "+ Nuovo gruppo…", grp_none: "Nessun gruppo", hd_all: "Nascondi tutto", hd_cats: "Categorie", hd_clear: "Elimina tutte le mie regole", hd_details: "Dettagli", hd_g_chat: "Chat", hd_g_site: "Sito", hd_g_win: "Pop-up", hd_hidden: "Nascosti {n} di {m}", hd_na: "non ancora trovato", hd_none: "Ancora nessuna regola.", hd_off: "Mostra tutto", hd_p_clean: "Player pulito", hd_p_min: "Minimo", hd_peek: "Tieni premuto un elemento per vedere cosa nasconde", hd_pick: "Scegli elemento", hd_reset: "Ripristina per questo sito", hd_rules: "Le mie regole", hd_title: "Nascondi", hide: "Nascondi (H)", hints: "M=AUDIO F=SCHERMO T=CINEMA H=NASCONDI", hist_clear: "Svuota", hist_clear_q: "Eliminare tutta la cronologia di visione?", hist_del: "Rimuovi dalla cronologia", hist_empty: "Ancora vuota. Il tempo viene conteggiato mentre il video è in riproduzione.", hist_s_last: "Recenti", hist_s_time: "Tempo", hist_s_vis: "Visite", hist_title: "Cronologia", hist_total: "Modelle: {n} · Totale: {t}", hist_visits: "visite", home: "Home", lang_auto: "Automatica", lang_title: "Lingua", min: "Riduci", mv_abr_row: "Qualità automatica", mv_abr_t: "Qualità automatica: riduce la qualità in caso di scatti o rete debole e la aumenta quando la connessione è stabile", mv_add: "Aggiungi", mv_auto: "Auto", mv_auto_all: "Aggiungi automaticamente tutti gli stream in diretta", mv_back: "Indietro", mv_bg: "Riproduci in background", mv_bg_t: "Gli stream continuano a riprodursi quando cambi scheda, chiudi questa finestra o esci dal browser", mv_bm_h: "Preferiti in diretta", mv_clear_q: "Chiudere tutti gli stream?", mv_close: "Chiudi", mv_cols: "Griglia", mv_cur: "Stream attuale", mv_dvr: "Buffer di riavvolgimento", mv_dvr_off: "Disattivato", mv_fs: "Schermo intero", mv_full: "La finestra è piena", mv_go: "Aggiungi", mv_hint: "Aggiungi stream per guardarne più di uno insieme. Tocca un riquadro per portarci l’audio.", mv_link_h: "Oppure tramite link", mv_load: "Caricamento…", mv_min_u: "min", mv_mute: "Disattiva audio", mv_none: "Nessun preferito in diretta", mv_off: "Stream non disponibile", mv_pip: "Picture-in-picture", mv_play: "Riproduci / pausa", mv_qual: "Qualità", mv_retry: "tocca per riprovare", mv_search: "Cerca tra i preferiti in diretta", mv_title: "Player", no_vid: "NO VIDEO", note_ph: "Nota... (#tag)", pip: "PIP", pk_cancel: "Annulla", pk_found: "corrisp.", pk_hint: "Tocca un elemento della pagina", pk_ok: "Nascondi", pk_up: "Su", qual: "QUAL.", rad_media: "Media", rad_nav: "Preferiti", rad_next: "Succ.", rad_page: "Pagina", rad_prev: "Prec.", rad_reset: "Reimposta posizione del pulsante", rad_title: "Menu radiale", rad_vdn: "Più piano", rad_view: "Schermo", rad_vup: "Più forte", rec: "REC", rec_tip: "Tocco: registra · Tieni premuto: impostazioni", rel: "Ricarica", rs_cur: "Stream attuale:", rs_gbh: "GB/h", rs_hint: "Quando una parte raggiunge la dimensione scelta, viene salvata come file separato e la registrazione continua in uno nuovo. I dati vengono scritti su disco in piccoli blocchi; se il disco non è disponibile, restano nella memoria del telefono, quindi conviene scegliere parti più piccole.", rs_mbmin: "MB/min", rs_next: "Le modifiche valgono dalla parte successiva o da una nuova registrazione.", rs_now: "Ora", rs_off: "Disattivato", rs_p: "parte", rs_part: "Dimensione parte (file diviso)", rs_q_hi: "Alta", rs_q_low: "Bassa", rs_q_mid: "Media", rs_q_min: "Risparmio", rs_quality: "Qualità (bitrate)", rs_recover: "Recupera le registrazioni non salvate", rs_resnote: "La cattura registra alla risoluzione dello stream: le opzioni sopra cambiano solo il bitrate (compressione), e 1080p–360p indicano la qualità d’immagine prevista. In modalità HLS viene scelto uno stream di altezza non superiore a quella indicata.", rs_src: "Sorgente", rs_src_cap: "Acquisizione (ricodifica)", rs_src_hls: "HLS (senza ricodifica)", rs_src_note: "HLS scarica i segmenti dello stream così come sono, senza ricodifica, e salva in MP4. Serve l’indirizzo dello stream; se non è disponibile, viene usata l’acquisizione. La qualità è quella originale dello stream di origine. La registrazione viene scritta su disco a piccoli passi.", rs_t: "totale", rs_title: "Impostazioni di registrazione", rs_total: "Ferma alla dimensione totale", rs_upto: "fino a", search: "CERCA", seen_now: "proprio ora", seen_tip: "Ultimo accesso", settings: "Impostazioni", sleep_custom: "Tempo personalizzato", sleep_hint: "Allo scadere del tempo il player si spegne e ogni registrazione si interrompe e viene salvata. Tocca lo schermo nero per riattivarlo.", sleep_left: "Rimanente", sleep_sec: "sec", sleep_start: "Avvia", sleep_title: "Timer di spegnimento", sleep_wake: "Timer terminato.\nTocca per continuare", snap: "FOTO", sort_date: "Data", sort_last: "Visto", sort_live: "Live", sort_name: "Nome", sort_viewers: "Spettatori", sound: "AUDIO", st_av_refresh: "Aggiorna avatar", st_away: "BRB", st_back: "Indietro", st_btns: "Pulsanti del pannello", st_btns_hint: "Tocca per nascondere un pulsante o riportarlo.", st_btns_reset: "Mostra tutti i pulsanti", st_color: "Colore del pannello", st_color_rnd: "Casuale", st_cols: "Colonne", st_custom: "Colore personalizzato", st_data: "Dati", st_fav: "Preferiti", st_favnav: "Pulsanti «precedente / successivo» sul telecomando", st_group: "Gruppo", st_hide_grp: "Nascondi il selettore di gruppo", st_hide_note: "Nascondi il campo note (tag)", st_hide_topic: "Nascondi la descrizione della modella", st_hist: "Cronologia di visione", st_hist_on: "Salva cronologia", st_hue: "Tonalità", st_lang: "Lingua", st_lig: "Luminosità", st_list: "Elenco", st_look: "Aspetto", st_open: "Apri", st_prev: "Anteprime delle modelle", st_prev_off: "Nessun aggiornamento", st_prev_spd: "Aggiornamento anteprime", st_private: "Privato", st_ptags: "Nascondi i tag del profilo (BongaCams)", st_rec: "Registrazione video", st_sat: "Saturazione", st_scale: "Dimensione del pannello", st_site_dark: "Tema scuro del sito", st_star: "Stella sulle miniature", st_vip: "VIP", star_add: "Aggiungi ai preferiti", star_del: "Rimuovi dai preferiti", star_del_q: "Questa modella ha una nota o un gruppo. Rimuoverla dai preferiti?", thtr: "CINEMA", tp_less: "[Meno]", tp_more: "[Altro]", u_d: "g", u_gb: "GB", u_h: "h", u_m: "min", u_mb: "MB", viewers_tip: "Spettatori nella stanza", w_new: "Non visto", w_seen: "Visto", }, PL: { ago: "{n} temu", bg: "TŁO", bk_csv: "Tabele CSV", bk_csv_fav: "Ulubione", bk_csv_hist: "Historia", bk_csv_ses: "Sesje", bk_exp: "Eksport kopii zapasowej", bk_imp: "Import kopii zapasowej", bk_p_fav: "Ulubione", bk_p_hist: "Historia", bk_p_set: "Ustawienia", bk_parts: "Zawartość kopii", bm_add: "Dodaj do ulubionych", bm_all: "Wszystkie", bm_del: "Usuń", bm_dup: "Już dodano!", bm_empty: "Lista jest pusta.", bm_find: "Szukaj po nazwie…", bm_nomatch: "Brak modelek pasujących do filtra", bm_online: "Online", bm_online_t: "Pokaż tylko online", bm_pin: "Przypnij na górze", bm_priv: "Priv", bm_title: "Ulubione", bm_unpin: "Odepnij", bt_hints: "Podpowiedzi", bt_stats: "Widzowie", bt_vol: "Głośność", cal_all_tag: "Wszystkie tagi", cal_allday: "Cały dzień", cal_c_dur: "Czas", cal_c_model: "Modelka", cal_c_n: "Wejścia", cal_c_time: "Wejście", cal_hist: "Historia", cal_loaded: "Grafik wczytany: {a} z {b}", cal_models: "Modelki", cal_no_sched: "Grafik jest pobierany z profili ulubionych. Otwórz Ulubione i poczekaj kilka sekund.", cal_none: "Tego dnia nic nie ma", cal_reset: "Resetuj", cal_sched: "Grafik", cal_title: "Kalendarz", cal_today: "Dziś", cal_total: "Razem", cal_watched: "Oglądane", cf_cancel: "Anuluj", cf_ok: "Tak", ch_avg: "Średnia dzienna", ch_btn: "Wykresy", ch_days: "Czas dziennie, 14 dni", ch_hours: "Aktywność według godzin", ch_peak: "Szczyt", ch_top: "Top według czasu", copy: "LINK", dark: "Motyw ciemny / jasny", don_copied: "Skopiowano adres", don_fail: "Nie udało się skopiować", don_sub: "Dzięki, że korzystasz", don_tap: "Dotknij adresu, aby zobaczyć go w całości. Przycisk po prawej go kopiuje.", don_tip: "Wesprzyj autora", don_title: "Postaw autorowi kawę", drag: "⁞⁞ PILOT", err: "BŁĄD", favs: "ULUB.", fr_hold: "Przytrzymaj, aby zobaczyć bieżącą klatkę", fr_none: "Brak klatki: modelka jest offline", fs: "PEŁNY", grp_all: "Wszystkie grupy", grp_new: "+ Nowa grupa…", grp_none: "Bez grupy", hd_all: "Ukryj wszystko", hd_cats: "Kategorie", hd_clear: "Usuń wszystkie moje reguły", hd_details: "Szczegóły", hd_g_chat: "Czat", hd_g_site: "Witryna", hd_g_win: "Wyskakujące okna", hd_hidden: "Ukryto {n} z {m}", hd_na: "jeszcze nie znaleziono", hd_none: "Na razie brak reguł.", hd_off: "Pokaż wszystko", hd_p_clean: "Czysty odtwarzacz", hd_p_min: "Minimum", hd_peek: "Przytrzymaj pozycję, aby zobaczyć, co ukrywa", hd_pick: "Wybierz element", hd_reset: "Resetuj dla tej strony", hd_rules: "Moje reguły", hd_title: "Ukrywanie", hide: "Ukryj (H)", hints: "M=DŹWIĘK F=PEŁNY T=KINO H=UKRYJ", hist_clear: "Wyczyść", hist_clear_q: "Usunąć całą historię oglądania?", hist_del: "Usuń z historii", hist_empty: "Na razie pusto. Czas jest liczony, gdy wideo jest odtwarzane.", hist_s_last: "Ostatnie", hist_s_time: "Czas", hist_s_vis: "Wizyty", hist_title: "Historia", hist_total: "Modelki: {n} · Razem: {t}", hist_visits: "wizyt", home: "Strona główna", lang_auto: "Automatycznie", lang_title: "Język", min: "Minimalizuj", mv_abr_row: "Jakość automatyczna", mv_abr_t: "Jakość automatyczna: obniża jakość przy zacięciach lub słabej sieci i podnosi, gdy połączenie jest stabilne", mv_add: "Dodaj", mv_auto: "Auto", mv_auto_all: "Automatycznie dodawaj wszystkie transmisje na żywo", mv_back: "Wstecz", mv_bg: "Odtwarzaj w tle", mv_bg_t: "Transmisje grają dalej po przełączeniu karty, zamknięciu tego okna lub wyjściu z przeglądarki", mv_bm_h: "Ulubione na żywo", mv_clear_q: "Zamknąć wszystkie transmisje?", mv_close: "Zamknij", mv_cols: "Układ", mv_cur: "Bieżąca transmisja", mv_dvr: "Bufor przewijania", mv_dvr_off: "Wył.", mv_fs: "Pełny ekran", mv_full: "Okno jest pełne", mv_go: "Dodaj", mv_hint: "Dodaj transmisje, aby oglądać kilka naraz. Dotknij kafelka, aby przenieść na niego dźwięk.", mv_link_h: "Lub przez link", mv_load: "Ładowanie…", mv_min_u: "min", mv_mute: "Wycisz", mv_none: "Brak ulubionych na żywo", mv_off: "Transmisja niedostępna", mv_pip: "Obraz w obrazie", mv_play: "Odtwarzaj / pauza", mv_qual: "Jakość", mv_retry: "dotknij, aby spróbować ponownie", mv_search: "Szukaj w ulubionych na żywo", mv_title: "Odtwarzacz", no_vid: "BRAK WIDEO", note_ph: "Notatka... (#tagi)", pip: "PIP", pk_cancel: "Anuluj", pk_found: "dopas.", pk_hint: "Dotknij elementu na stronie", pk_ok: "Ukryj", pk_up: "Wyżej", qual: "JAKOŚĆ", rad_media: "Media", rad_nav: "Ulubione", rad_next: "Nast.", rad_page: "Strona", rad_prev: "Poprz.", rad_reset: "Resetuj pozycję przycisku", rad_title: "Menu kołowe", rad_vdn: "Ciszej", rad_view: "Widok", rad_vup: "Głośniej", rec: "NAGR.", rec_tip: "Dotknięcie: nagrywaj · Przytrzymanie: ustawienia", rel: "Odśwież", rs_cur: "Obecny strumień:", rs_gbh: "GB/godz.", rs_hint: "Gdy część osiągnie wybrany rozmiar, jest zapisywana jako osobny plik, a nagrywanie trwa w nowym. Dane są zapisywane na dysk małymi porcjami; jeśli dysk jest niedostępny, zostają w pamięci telefonu, więc lepiej wybrać mniejsze części.", rs_mbmin: "MB/min", rs_next: "Zmiany obowiązują od następnej części lub nowego nagrania.", rs_now: "Teraz", rs_off: "Wył.", rs_p: "część", rs_part: "Rozmiar części (plik jest dzielony)", rs_q_hi: "Wysoka", rs_q_low: "Niska", rs_q_mid: "Średnia", rs_q_min: "Oszczędna", rs_quality: "Jakość (bitrate)", rs_recover: "Odzyskaj niezapisane nagrania", rs_resnote: "Przechwytywanie nagrywa w rozdzielczości strumienia: opcje powyżej zmieniają tylko bitrate (kompresję), a 1080p–360p pokazują oczekiwaną jakość obrazu. W trybie HLS wybierany jest strumień o wysokości nie większej niż wskazana.", rs_src: "Źródło", rs_src_cap: "Przechwytywanie (ponowne kodowanie)", rs_src_hls: "HLS (bez ponownego kodowania)", rs_src_note: "HLS pobiera segmenty strumienia bez zmian, bez ponownego kodowania, i zapisuje MP4. Potrzebny jest adres strumienia; jeśli go brak, używane jest przechwytywanie. Jakość jest oryginalna, taka jak w strumieniu źródłowym. Nagranie jest zapisywane na dysku małymi porcjami.", rs_t: "razem", rs_title: "Ustawienia nagrywania", rs_total: "Zatrzymaj przy łącznym rozmiarze", rs_upto: "do", search: "SZUKAJ", seen_now: "przed chwilą", seen_tip: "Ostatnio online", settings: "Ustawienia", sleep_custom: "Własny czas", sleep_hint: "Po upływie czasu odtwarzacz zostanie wyłączony, a nagrywanie zatrzymane i zapisane. Dotknij czarnego ekranu, aby go przywrócić.", sleep_left: "Pozostało", sleep_sec: "sek", sleep_start: "Start", sleep_title: "Wyłącznik czasowy", sleep_wake: "Czas minął.\nDotknij, aby kontynuować", snap: "FOTO", sort_date: "Data", sort_last: "Widz.", sort_live: "Live", sort_name: "Nazwa", sort_viewers: "Widzowie", sound: "DŹWIĘK", st_av_refresh: "Odśwież awatary", st_away: "BRB", st_back: "Wstecz", st_btns: "Przyciski panelu", st_btns_hint: "Dotknij, aby ukryć przycisk lub go przywrócić.", st_btns_reset: "Pokaż wszystkie przyciski", st_color: "Kolor panelu", st_color_rnd: "Losowy", st_cols: "Kolumny", st_custom: "Własny kolor", st_data: "Dane", st_fav: "Ulubione", st_favnav: "Przyciski „wstecz / dalej” na pilocie", st_group: "Grupowy", st_hide_grp: "Ukryj wybór grupy", st_hide_note: "Ukryj pole notatki (tagi)", st_hide_topic: "Ukryj opis modelki", st_hist: "Historia oglądania", st_hist_on: "Zapisuj historię", st_hue: "Odcień", st_lang: "Język", st_lig: "Jasność", st_list: "Lista", st_look: "Wygląd", st_open: "Otwórz", st_prev: "Podglądy modelek", st_prev_off: "Bez odświeżania", st_prev_spd: "Odświeżanie podglądu", st_private: "Prywatny", st_ptags: "Ukryj tagi profilu (BongaCams)", st_rec: "Nagrywanie wideo", st_sat: "Nasycenie", st_scale: "Rozmiar panelu", st_site_dark: "Ciemny motyw strony", st_star: "Gwiazdka na miniaturach", st_vip: "VIP", star_add: "Dodaj do ulubionych", star_del: "Usuń z ulubionych", star_del_q: "Ta modelka ma notatkę lub grupę. Usunąć z ulubionych?", thtr: "KINO", tp_less: "[Mniej]", tp_more: "[Więcej]", u_d: "d", u_gb: "GB", u_h: "godz.", u_m: "min", u_mb: "MB", viewers_tip: "Widzowie w pokoju", w_new: "Nieoglądane", w_seen: "Oglądane", }, TR: { ago: "{n} önce", bg: "ARKA", bk_csv: "CSV tabloları", bk_csv_fav: "Favoriler", bk_csv_hist: "Geçmiş", bk_csv_ses: "Oturumlar", bk_exp: "Yedeği dışa aktar", bk_imp: "Yedeği içe aktar", bk_p_fav: "Favoriler", bk_p_hist: "Geçmiş", bk_p_set: "Ayarlar", bk_parts: "Yedek içeriği", bm_add: "Favorilere ekle", bm_all: "Tümü", bm_del: "Sil", bm_dup: "Zaten eklendi!", bm_empty: "Liste boş.", bm_find: "İsme göre ara…", bm_nomatch: "Filtreye uyan model yok", bm_online: "Çevrimiçi", bm_online_t: "Yalnızca çevrimiçi olanları göster", bm_pin: "En üste sabitle", bm_priv: "Özel", bm_title: "Favoriler", bm_unpin: "Sabitlemeyi kaldır", bt_hints: "İpuçları", bt_stats: "İzleyiciler", bt_vol: "Ses", cal_all_tag: "Tüm etiketler", cal_allday: "Tüm gün", cal_c_dur: "Süre", cal_c_model: "Model", cal_c_n: "Ziyaret", cal_c_time: "Giriş", cal_hist: "Geçmiş", cal_loaded: "Program yüklendi: {a} / {b}", cal_models: "Modeller", cal_no_sched: "Program, favorilerinizin profillerinden alınır. Favoriler'i açın ve birkaç saniye bekleyin.", cal_none: "Bu gün boş", cal_reset: "Sıfırla", cal_sched: "Program", cal_title: "Takvim", cal_today: "Bugün", cal_total: "Toplam", cal_watched: "İzlendi", cf_cancel: "İptal", cf_ok: "Evet", ch_avg: "Günlük ortalama", ch_btn: "Grafikler", ch_days: "Günlük süre, 14 gün", ch_hours: "Saate göre etkinlik", ch_peak: "Zirve", ch_top: "Süreye göre en çok", copy: "LİNK", dark: "Koyu / açık tema", don_copied: "Adres kopyalandı", don_fail: "Kopyalanamadı", don_sub: "Kullandığın için teşekkürler", don_tap: "Tamamını görmek için bir adrese dokun. Sağdaki düğme adresi kopyalar.", don_tip: "Yazarı destekle", don_title: "Yazara bir kahve ısmarla", drag: "⁞⁞ KUMANDA", err: "HATA", favs: "FAVORİ", fr_hold: "Geçerli kareyi görmek için basılı tut", fr_none: "Kare yok: model çevrimdışı", fs: "TAM", grp_all: "Tüm gruplar", grp_new: "+ Yeni grup…", grp_none: "Grupsuz", hd_all: "Hepsini gizle", hd_cats: "Kategoriler", hd_clear: "Tüm kurallarımı sil", hd_details: "Ayrıntılar", hd_g_chat: "Sohbet", hd_g_site: "Site", hd_g_win: "Açılır pencereler", hd_hidden: "{m} öğeden {n} gizli", hd_na: "henüz bulunamadı", hd_none: "Henüz kural yok.", hd_off: "Hepsini göster", hd_p_clean: "Temiz oynatıcı", hd_p_min: "Minimum", hd_peek: "Neyi gizlediğini görmek için bir öğeye basılı tutun", hd_pick: "Öğe seç", hd_reset: "Bu site için sıfırla", hd_rules: "Kurallarım", hd_title: "Gizleme", hide: "Gizle (H)", hints: "M=SES F=TAM T=SİNEMA H=GİZLE", hist_clear: "Temizle", hist_clear_q: "Tüm izleme geçmişi silinsin mi?", hist_del: "Geçmişten kaldır", hist_empty: "Şimdilik boş. Süre, video oynarken sayılır.", hist_s_last: "Son", hist_s_time: "Süre", hist_s_vis: "Ziyaret", hist_title: "Geçmiş", hist_total: "Modeller: {n} · Toplam: {t}", hist_visits: "ziyaret", home: "Ana sayfa", lang_auto: "Otomatik", lang_title: "Dil", min: "Küçült", mv_abr_row: "Otomatik kalite", mv_abr_t: "Otomatik kalite: video takıldığında veya ağ zayıfladığında kaliteyi düşürür, bağlantı kararlı olduğunda yükseltir", mv_add: "Ekle", mv_auto: "Otomatik", mv_auto_all: "Tüm canlı yayınları otomatik ekle", mv_back: "Geri", mv_bg: "Arka planda oynat", mv_bg_t: "Sekme değiştirdiğinizde, bu pencereyi kapattığınızda veya tarayıcıdan çıktığınızda yayınlar oynamaya devam eder", mv_bm_h: "Canlı yer imleri", mv_clear_q: "Tüm yayınlar kapatılsın mı?", mv_close: "Kapat", mv_cols: "Düzen", mv_cur: "Mevcut yayın", mv_dvr: "Geri sarma tamponu", mv_dvr_off: "Kapalı", mv_fs: "Tam ekran", mv_full: "Pencere dolu", mv_go: "Ekle", mv_hint: "Aynı anda birkaç yayın izlemek için yayın ekleyin. Sesi bir kutucuğa taşımak için ona dokunun.", mv_link_h: "Veya bağlantıyla", mv_load: "Yükleniyor…", mv_min_u: "dk", mv_mute: "Sessize al", mv_none: "Canlı yer imi yok", mv_off: "Yayın kullanılamıyor", mv_pip: "Resim içinde resim", mv_play: "Oynat / duraklat", mv_qual: "Kalite", mv_retry: "tekrar denemek için dokunun", mv_search: "Canlı yer imlerinde ara", mv_title: "Oynatıcı", no_vid: "VİDEO YOK", note_ph: "Not... (#etiket)", pip: "PIP", pk_cancel: "İptal", pk_found: "eşleşme", pk_hint: "Sayfadaki bir öğeye dokunun", pk_ok: "Gizle", pk_up: "Yukarı", qual: "KALİTE", rad_media: "Medya", rad_nav: "Favoriler", rad_next: "Sonraki", rad_page: "Sayfa", rad_prev: "Önceki", rad_reset: "Düğme konumunu sıfırla", rad_title: "Dairesel menü", rad_vdn: "Daha kısık", rad_view: "Ekran", rad_vup: "Daha yüksek", rec: "KAYIT", rec_tip: "Dokun: kaydet · Basılı tut: ayarlar", rel: "Yenile", rs_cur: "Mevcut yayın:", rs_gbh: "GB/sa", rs_hint: "Bir parça seçilen boyuta ulaştığında ayrı bir dosya olarak kaydedilir ve kayıt yeni bir parçada devam eder. Veriler diske küçük bloklar halinde yazılır; disk kullanılamazsa telefonun belleğinde kalır, bu durumda daha küçük parçalar seçmek iyi olur.", rs_mbmin: "MB/dk", rs_next: "Değişiklikler bir sonraki parçadan veya yeni kayıttan itibaren geçerli olur.", rs_now: "Şimdi", rs_off: "Kapalı", rs_p: "parça", rs_part: "Parça boyutu (dosya bölünür)", rs_q_hi: "Yüksek", rs_q_low: "Düşük", rs_q_mid: "Orta", rs_q_min: "Tasarruf", rs_quality: "Kalite (bit hızı)", rs_recover: "Kaydedilmemiş kayıtları kurtar", rs_resnote: "Yakalama modu yayının çözünürlüğünde kaydeder: yukarıdaki seçenekler yalnızca bit hızını (sıkıştırmayı) değiştirir; 1080p–360p beklenen görüntü kalitesini gösterir. HLS modunda, seçilen yükseklikten büyük olmayan bir yayın seçilir.", rs_src: "Kaynak", rs_src_cap: "Yakalama (yeniden kodlama)", rs_src_hls: "HLS (yeniden kodlamasız)", rs_src_note: "HLS, yayın parçalarını olduğu gibi, yeniden kodlamadan indirir ve MP4 olarak kaydeder. Yayın adresi gerekir; yoksa yakalama kullanılır. Kalite, kaynak yayınla aynı, yani orijinaldir. Kayıt diske küçük kontrol noktalarıyla yazılır.", rs_t: "toplam", rs_title: "Kayıt ayarları", rs_total: "Toplam boyutta durdur", rs_upto: "en fazla", search: "ARA", seen_now: "az önce", seen_tip: "Son çevrimiçi", settings: "Ayarlar", sleep_custom: "Özel süre", sleep_hint: "Süre dolunca oynatıcı kapanır, varsa kayıt durdurulur ve kaydedilir. Geri getirmek için siyah ekrana dokunun.", sleep_left: "Kalan", sleep_sec: "sn", sleep_start: "Başlat", sleep_title: "Uyku zamanlayıcısı", sleep_wake: "Zamanlayıcı bitti.\nDevam etmek için dokunun", snap: "FOTO", sort_date: "Tarih", sort_last: "Son", sort_live: "Canlı", sort_name: "Ad", sort_viewers: "İzleyiciler", sound: "SES", st_av_refresh: "Avatarları yenile", st_away: "BRB", st_back: "Geri", st_btns: "Panel düğmeleri", st_btns_hint: "Bir düğmeyi gizlemek veya geri getirmek için dokunun.", st_btns_reset: "Tüm düğmeleri göster", st_color: "Panel rengi", st_color_rnd: "Rastgele", st_cols: "Sütunlar", st_custom: "Özel renk", st_data: "Veri", st_fav: "Favoriler", st_favnav: "Kumandada «geri / ileri» düğmeleri", st_group: "Grup", st_hide_grp: "Grup seçicisini gizle", st_hide_note: "Not alanını gizle (etiketler)", st_hide_topic: "Model açıklamasını gizle", st_hist: "İzleme geçmişi", st_hist_on: "Geçmişi tut", st_hue: "Ton", st_lang: "Dil", st_lig: "Parlaklık", st_list: "Liste", st_look: "Görünüm", st_open: "Aç", st_prev: "Model önizlemeleri", st_prev_off: "Yenileme yok", st_prev_spd: "Önizleme yenileme", st_private: "Özel", st_ptags: "Profil etiketlerini gizle (BongaCams)", st_rec: "Video kaydı", st_sat: "Doygunluk", st_scale: "Panel boyutu", st_site_dark: "Sitenin koyu teması", st_star: "Küçük resimlerde yıldız", st_vip: "VIP", star_add: "Favorilere ekle", star_del: "Favorilerden kaldır", star_del_q: "Bu modelin notu veya grubu var. Favorilerden kaldırılsın mı?", thtr: "SİNEMA", tp_less: "[Daha az]", tp_more: "[Daha fazla]", u_d: "g", u_gb: "GB", u_h: "sa", u_m: "dk", u_mb: "MB", viewers_tip: "Odadaki izleyiciler", w_new: "İzlenmedi", w_seen: "İzlendi", }, NL: { ago: "{n} geleden", bg: "ACHT.", bk_csv: "CSV-tabellen", bk_csv_fav: "Favorieten", bk_csv_hist: "Geschiedenis", bk_csv_ses: "Sessies", bk_exp: "Back-up exporteren", bk_imp: "Back-up importeren", bk_p_fav: "Favorieten", bk_p_hist: "Geschiedenis", bk_p_set: "Instellingen", bk_parts: "Inhoud van de back-up", bm_add: "Toevoegen aan favorieten", bm_all: "Alle", bm_del: "Verwijderen", bm_dup: "Al toegevoegd!", bm_empty: "De lijst is leeg.", bm_find: "Zoeken op naam…", bm_nomatch: "Geen modellen voldoen aan het filter", bm_online: "Online", bm_online_t: "Alleen online tonen", bm_pin: "Bovenaan vastzetten", bm_priv: "Priv", bm_title: "Favorieten", bm_unpin: "Losmaken", bt_hints: "Tips", bt_stats: "Kijkers", bt_vol: "Volume", cal_all_tag: "Alle tags", cal_allday: "Hele dag", cal_c_dur: "Duur", cal_c_model: "Model", cal_c_n: "Bezoeken", cal_c_time: "Binnenkomst", cal_hist: "Geschiedenis", cal_loaded: "Rooster geladen: {a} van {b}", cal_models: "Modellen", cal_no_sched: "Het rooster komt uit de profielen van je favorieten. Open Favorieten en wacht een paar seconden.", cal_none: "Niets op deze dag", cal_reset: "Resetten", cal_sched: "Rooster", cal_title: "Kalender", cal_today: "Vandaag", cal_total: "Totaal", cal_watched: "Bekeken", cf_cancel: "Annuleren", cf_ok: "Ja", ch_avg: "Daggemiddelde", ch_btn: "Grafieken", ch_days: "Tijd per dag, 14 dagen", ch_hours: "Activiteit per uur", ch_peak: "Piek", ch_top: "Top op tijd", copy: "LINK", dark: "Donker / licht thema", don_copied: "Adres gekopieerd", don_fail: "Kopiëren mislukt", don_sub: "Bedankt voor het gebruik", don_tap: "Tik op een adres om het volledig te zien. De knop rechts kopieert het.", don_tip: "Steun de auteur", don_title: "Trakteer de auteur op een koffie", drag: "⁞⁞ BEDIENING", err: "FOUT", favs: "FAVO'S", fr_hold: "Houd ingedrukt voor het huidige beeld", fr_none: "Geen beeld: model is offline", fs: "VOL", grp_all: "Alle groepen", grp_new: "+ Nieuwe groep…", grp_none: "Geen groep", hd_all: "Alles verbergen", hd_cats: "Categorieën", hd_clear: "Al mijn regels wissen", hd_details: "Details", hd_g_chat: "Chat", hd_g_site: "Site", hd_g_win: "Pop-ups", hd_hidden: "{n} van {m} verborgen", hd_na: "nog niet gevonden", hd_none: "Nog geen regels.", hd_off: "Alles tonen", hd_p_clean: "Schone speler", hd_p_min: "Minimaal", hd_peek: "Houd een item ingedrukt om te zien wat het verbergt", hd_pick: "Element kiezen", hd_reset: "Resetten voor deze site", hd_rules: "Mijn regels", hd_title: "Verbergen", hide: "Verbergen (H)", hints: "M=GELUID F=VOL T=THEATER H=VERBERG", hist_clear: "Wissen", hist_clear_q: "De hele kijkgeschiedenis verwijderen?", hist_del: "Verwijderen uit geschiedenis", hist_empty: "Nog leeg. De tijd wordt geteld terwijl de video speelt.", hist_s_last: "Recent", hist_s_time: "Tijd", hist_s_vis: "Bezoeken", hist_title: "Geschiedenis", hist_total: "Modellen: {n} · Totaal: {t}", hist_visits: "bezoeken", home: "Home", lang_auto: "Auto", lang_title: "Taal", min: "Minimaliseren", mv_abr_row: "Automatische kwaliteit", mv_abr_t: "Automatische kwaliteit: verlaagt de kwaliteit bij haperingen of een zwak netwerk en verhoogt hem als de verbinding stabiel is", mv_add: "Toevoegen", mv_auto: "Auto", mv_auto_all: "Alle live streams automatisch toevoegen", mv_back: "Terug", mv_bg: "Afspelen op de achtergrond", mv_bg_t: "Streams blijven spelen als je van tabblad wisselt, dit venster sluit of de browser verlaat", mv_bm_h: "Live bladwijzers", mv_clear_q: "Alle streams sluiten?", mv_close: "Sluiten", mv_cols: "Indeling", mv_cur: "Huidige stream", mv_dvr: "Terugspoelbuffer", mv_dvr_off: "Uit", mv_fs: "Volledig scherm", mv_full: "Venster is vol", mv_go: "Toevoegen", mv_hint: "Voeg streams toe om er meerdere tegelijk te bekijken. Tik op een tegel om het geluid ernaartoe te verplaatsen.", mv_link_h: "Of via link", mv_load: "Laden…", mv_min_u: "min", mv_mute: "Dempen", mv_none: "Geen live bladwijzers", mv_off: "Stream niet beschikbaar", mv_pip: "Beeld-in-beeld", mv_play: "Afspelen / pauzeren", mv_qual: "Kwaliteit", mv_retry: "tik om opnieuw te proberen", mv_search: "Live bladwijzers zoeken", mv_title: "Speler", no_vid: "GEEN VIDEO", note_ph: "Notitie... (#tags)", pip: "PIP", pk_cancel: "Annuleren", pk_found: "treffers", pk_hint: "Tik op een element op de pagina", pk_ok: "Verbergen", pk_up: "Hoger", qual: "KWAL.", rad_media: "Media", rad_nav: "Favorieten", rad_next: "Volgende", rad_page: "Pagina", rad_prev: "Vorige", rad_reset: "Knoppositie resetten", rad_title: "Radiaal menu", rad_vdn: "Zachter", rad_view: "Weergave", rad_vup: "Harder", rec: "OPN.", rec_tip: "Tik: opnemen · Vasthouden: instellingen", rel: "Herladen", rs_cur: "Huidige stream:", rs_gbh: "GB/u", rs_hint: "Wanneer een deel de gekozen grootte bereikt, wordt het als apart bestand opgeslagen en gaat de opname verder in een nieuw bestand. Gegevens worden in kleine blokken naar de schijf geschreven; is de schijf niet beschikbaar, dan blijven ze in het telefoongeheugen, kies dan kleinere delen.", rs_mbmin: "MB/min", rs_next: "Wijzigingen gelden vanaf het volgende deel of een nieuwe opname.", rs_now: "Nu", rs_off: "Uit", rs_p: "deel", rs_part: "Deelgrootte (bestand wordt gesplitst)", rs_q_hi: "Hoog", rs_q_low: "Laag", rs_q_mid: "Gemiddeld", rs_q_min: "Besparen", rs_quality: "Kwaliteit (bitrate)", rs_recover: "Niet-opgeslagen opnames herstellen", rs_resnote: "Capture neemt op in de resolutie van de stream: de opties hierboven wijzigen alleen de bitrate (compressie), en 1080p–360p geven de verwachte beeldkwaliteit aan. In HLS-modus wordt een stream gekozen die niet hoger is dan de opgegeven hoogte.", rs_src: "Bron", rs_src_cap: "Opname (hercodering)", rs_src_hls: "HLS (zonder hercodering)", rs_src_note: "HLS downloadt de streamsegmenten ongewijzigd, zonder hercodering, en slaat MP4 op. Het streamadres is nodig; is dat er niet, dan wordt opname gebruikt. De kwaliteit is origineel, zoals in de brondstream. De opname wordt in kleine tussenstappen naar schijf geschreven.", rs_t: "totaal", rs_title: "Opname-instellingen", rs_total: "Stop bij totale grootte", rs_upto: "tot", search: "ZOEK", seen_now: "zojuist", seen_tip: "Laatst online gezien", settings: "Instellingen", sleep_custom: "Eigen tijd", sleep_hint: "Als de tijd om is, wordt de speler uitgeschakeld en wordt een lopende opname gestopt en opgeslagen. Tik op het zwarte scherm om de speler terug te halen.", sleep_left: "Resterend", sleep_sec: "sec", sleep_start: "Starten", sleep_title: "Slaaptimer", sleep_wake: "Slaaptimer afgelopen.\nTik om door te gaan", snap: "FOTO", sort_date: "Datum", sort_last: "Gezien", sort_live: "Live", sort_name: "Naam", sort_viewers: "Kijkers", sound: "GELUID", st_av_refresh: "Avatars vernieuwen", st_away: "BRB", st_back: "Terug", st_btns: "Paneelknoppen", st_btns_hint: "Tik om een knop te verbergen of terug te zetten.", st_btns_reset: "Alle knoppen tonen", st_color: "Paneelkleur", st_color_rnd: "Willekeurig", st_cols: "Kolommen", st_custom: "Eigen kleur", st_data: "Gegevens", st_fav: "Favorieten", st_favnav: "«Vorige / volgende»-knoppen op de bediening", st_group: "Groep", st_hide_grp: "Groepskeuze verbergen", st_hide_note: "Notitieveld (tags) verbergen", st_hide_topic: "Modelbeschrijving verbergen", st_hist: "Kijkgeschiedenis", st_hist_on: "Geschiedenis bewaren", st_hue: "Tint", st_lang: "Taal", st_lig: "Helderheid", st_list: "Lijst", st_look: "Uiterlijk", st_open: "Openen", st_prev: "Voorbeelden van modellen", st_prev_off: "Niet vernieuwen", st_prev_spd: "Voorbeelden vernieuwen", st_private: "Privé", st_ptags: "Profieltags verbergen (BongaCams)", st_rec: "Video-opname", st_sat: "Verzadiging", st_scale: "Paneelgrootte", st_site_dark: "Donker thema van de site", st_star: "Ster op miniaturen", st_vip: "VIP", star_add: "Toevoegen aan favorieten", star_del: "Verwijderen uit favorieten", star_del_q: "Dit model heeft een notitie of groep. Verwijderen uit favorieten?", thtr: "THEATER", tp_less: "[Minder]", tp_more: "[Meer]", u_d: "d", u_gb: "GB", u_h: "u", u_m: "min", u_mb: "MB", viewers_tip: "Kijkers in de kamer", w_new: "Niet bekeken", w_seen: "Bekeken", }, CS: { ago: "před {n}", bg: "POZADÍ", bk_csv: "Tabulky CSV", bk_csv_fav: "Oblíbené", bk_csv_hist: "Historie", bk_csv_ses: "Relace", bk_exp: "Export zálohy", bk_imp: "Import zálohy", bk_p_fav: "Oblíbené", bk_p_hist: "Historie", bk_p_set: "Nastavení", bk_parts: "Obsah zálohy", bm_add: "Přidat do oblíbených", bm_all: "Vše", bm_del: "Smazat", bm_dup: "Už přidáno!", bm_empty: "Seznam je prázdný.", bm_find: "Hledat podle jména…", bm_nomatch: "Filtru neodpovídá žádná modelka", bm_online: "Online", bm_online_t: "Zobrazit jen online", bm_pin: "Připnout nahoru", bm_priv: "Priv", bm_title: "Oblíbené", bm_unpin: "Odepnout", bt_hints: "Tipy", bt_stats: "Diváci", bt_vol: "Hlasitost", cal_all_tag: "Všechny štítky", cal_allday: "Celý den", cal_c_dur: "Doba", cal_c_model: "Modelka", cal_c_n: "Vstupy", cal_c_time: "Vstup", cal_hist: "Historie", cal_loaded: "Rozvrh načten: {a} z {b}", cal_models: "Modelky", cal_no_sched: "Rozvrh se načítá z profilů oblíbených. Otevřete Oblíbené a chvíli počkejte.", cal_none: "V tento den nic není", cal_reset: "Obnovit", cal_sched: "Rozvrh", cal_title: "Kalendář", cal_today: "Dnes", cal_total: "Celkem", cal_watched: "Shlédnuto", cf_cancel: "Zrušit", cf_ok: "Ano", ch_avg: "Denní průměr", ch_btn: "Grafy", ch_days: "Čas za den, 14 dní", ch_hours: "Aktivita podle hodin", ch_peak: "Špička", ch_top: "Top podle času", copy: "ODKAZ", dark: "Tmavý / světlý motiv", don_copied: "Adresa zkopírována", don_fail: "Kopírování se nezdařilo", don_sub: "Děkuji, že to používáte", don_tap: "Klepnutím na adresu ji zobrazíte celou. Tlačítko vpravo ji zkopíruje.", don_tip: "Podpořit autora", don_title: "Kupte autorovi kávu", drag: "⁞⁞ OVLÁDÁNÍ", err: "CHYBA", favs: "OBLÍBENÉ", fr_hold: "Podržením zobrazíte aktuální snímek", fr_none: "Snímek není k dispozici: modelka je offline", fs: "CELÁ", grp_all: "Všechny skupiny", grp_new: "+ Nová skupina…", grp_none: "Bez skupiny", hd_all: "Skrýt vše", hd_cats: "Kategorie", hd_clear: "Smazat všechna moje pravidla", hd_details: "Podrobnosti", hd_g_chat: "Chat", hd_g_site: "Web", hd_g_win: "Vyskakovací okna", hd_hidden: "Skryto {n} z {m}", hd_na: "zatím nenalezeno", hd_none: "Zatím žádná pravidla.", hd_off: "Zobrazit vše", hd_p_clean: "Čistý přehrávač", hd_p_min: "Minimum", hd_peek: "Podržte položku, abyste viděli, co skrývá", hd_pick: "Vybrat prvek", hd_reset: "Obnovit pro tento web", hd_rules: "Moje pravidla", hd_title: "Skrývání", hide: "Skrýt (H)", hints: "M=ZVUK F=CELÁ T=KINO H=SKRÝT", hist_clear: "Vymazat", hist_clear_q: "Smazat celou historii sledování?", hist_del: "Odebrat z historie", hist_empty: "Zatím prázdné. Čas se počítá, dokud video běží.", hist_s_last: "Nedávné", hist_s_time: "Čas", hist_s_vis: "Návštěvy", hist_title: "Historie", hist_total: "Modelky: {n} · Celkem: {t}", hist_visits: "návštěv", home: "Domů", lang_auto: "Auto", lang_title: "Jazyk", min: "Minimalizovat", mv_abr_row: "Automatická kvalita", mv_abr_t: "Automatická kvalita: snižuje kvalitu při sekání nebo slabé síti a zvyšuje ji, když je připojení stabilní", mv_add: "Přidat", mv_auto: "Auto", mv_auto_all: "Automaticky přidat všechny živé streamy", mv_back: "Zpět", mv_bg: "Přehrávat na pozadí", mv_bg_t: "Streamy se přehrávají dál, když přepnete kartu, zavřete toto okno nebo opustíte prohlížeč", mv_bm_h: "Živé záložky", mv_clear_q: "Zavřít všechny streamy?", mv_close: "Zavřít", mv_cols: "Rozložení", mv_cur: "Aktuální stream", mv_dvr: "Vyrovnávací paměť pro přetáčení", mv_dvr_off: "Vyp.", mv_fs: "Celá obrazovka", mv_full: "Okno je plné", mv_go: "Přidat", mv_hint: "Přidejte streamy, abyste mohli sledovat několik najednou. Klepnutím na dlaždici na ni přesunete zvuk.", mv_link_h: "Nebo odkazem", mv_load: "Načítání…", mv_min_u: "min", mv_mute: "Ztlumit", mv_none: "Žádné živé záložky", mv_off: "Stream není dostupný", mv_pip: "Obraz v obraze", mv_play: "Přehrát / pozastavit", mv_qual: "Kvalita", mv_retry: "klepnutím to zkusíte znovu", mv_search: "Hledat v živých záložkách", mv_title: "Přehrávač", no_vid: "BEZ VIDEA", note_ph: "Poznámka... (#tagy)", pip: "PIP", pk_cancel: "Zrušit", pk_found: "shod", pk_hint: "Klepněte na prvek na stránce", pk_ok: "Skrýt", pk_up: "Výše", qual: "KVAL.", rad_media: "Média", rad_nav: "Oblíbené", rad_next: "Další", rad_page: "Stránka", rad_prev: "Předch.", rad_reset: "Obnovit pozici tlačítka", rad_title: "Kruhové menu", rad_vdn: "Tišeji", rad_view: "Zobrazení", rad_vup: "Hlasitěji", rec: "NAHR.", rec_tip: "Klepnutí: nahrávat · Podržení: nastavení", rel: "Obnovit", rs_cur: "Aktuální stream:", rs_gbh: "GB/h", rs_hint: "Když část dosáhne zvolené velikosti, uloží se jako samostatný soubor a nahrávání pokračuje v novém. Data se zapisují na disk po malých blocích; pokud disk není dostupný, zůstávají v paměti telefonu, a pak je lepší zvolit menší části.", rs_mbmin: "MB/min", rs_next: "Změny platí od další části nebo nové nahrávky.", rs_now: "Nyní", rs_off: "Vyp.", rs_p: "část", rs_part: "Velikost části (soubor se dělí)", rs_q_hi: "Vysoká", rs_q_low: "Nízká", rs_q_mid: "Střední", rs_q_min: "Úsporná", rs_quality: "Kvalita (bitrate)", rs_recover: "Obnovit neuložené nahrávky", rs_resnote: "Zachytávání nahrává v rozlišení streamu: možnosti výše mění jen bitrate (kompresi) a 1080p–360p ukazují očekávanou kvalitu obrazu. V režimu HLS se vybírá stream, který není vyšší než zadaná výška.", rs_src: "Zdroj", rs_src_cap: "Zachytávání (překódování)", rs_src_hls: "HLS (bez překódování)", rs_src_note: "HLS stahuje segmenty streamu tak, jak jsou, bez překódování, a ukládá MP4. Potřebuje adresu streamu; pokud není k dispozici, použije se zachytávání. Kvalita je původní, stejná jako ve zdrojovém streamu. Nahrávka se na disk zapisuje po malých částech.", rs_t: "celkem", rs_title: "Nastavení nahrávání", rs_total: "Zastavit při celkové velikosti", rs_upto: "až", search: "HLEDAT", seen_now: "právě teď", seen_tip: "Naposledy online", settings: "Nastavení", sleep_custom: "Vlastní čas", sleep_hint: "Po uplynutí času se přehrávač vypne a případné nahrávání se zastaví a uloží. Klepnutím na černou obrazovku ho vrátíte.", sleep_left: "Zbývá", sleep_sec: "s", sleep_start: "Spustit", sleep_title: "Časovač vypnutí", sleep_wake: "Časovač skončil.\nKlepnutím pokračujte", snap: "FOTO", sort_date: "Datum", sort_last: "Viděna", sort_live: "Live", sort_name: "Jméno", sort_viewers: "Diváci", sound: "ZVUK", st_av_refresh: "Obnovit avatary", st_away: "BRB", st_back: "Zpět", st_btns: "Tlačítka panelu", st_btns_hint: "Klepnutím tlačítko skryjete nebo vrátíte.", st_btns_reset: "Zobrazit všechna tlačítka", st_color: "Barva panelu", st_color_rnd: "Náhodná", st_cols: "Sloupce", st_custom: "Vlastní barva", st_data: "Data", st_fav: "Oblíbené", st_favnav: "Tlačítka „zpět / vpřed“ na ovladači", st_group: "Skupina", st_hide_grp: "Skrýt výběr skupiny", st_hide_note: "Skrýt pole poznámky (štítky)", st_hide_topic: "Skrýt popis modelky", st_hist: "Historie sledování", st_hist_on: "Ukládat historii", st_hue: "Odstín", st_lang: "Jazyk", st_lig: "Jas", st_list: "Seznam", st_look: "Vzhled", st_open: "Otevřít", st_prev: "Náhledy modelek", st_prev_off: "Neobnovovat", st_prev_spd: "Obnovování náhledů", st_private: "Privát", st_ptags: "Skrýt štítky profilu (BongaCams)", st_rec: "Nahrávání videa", st_sat: "Sytost", st_scale: "Velikost panelu", st_site_dark: "Tmavý motiv webu", st_star: "Hvězdička na náhledech", st_vip: "VIP", star_add: "Přidat do oblíbených", star_del: "Odebrat z oblíbených", star_del_q: "Tato modelka má poznámku nebo skupinu. Odebrat z oblíbených?", thtr: "KINO", tp_less: "[Méně]", tp_more: "[Více]", u_d: "d", u_gb: "GB", u_h: "h", u_m: "min", u_mb: "MB", viewers_tip: "Diváci v místnosti", w_new: "Neshlédnuto", w_seen: "Shlédnuto", }, RO: { ago: "acum {n}", bg: "FUNDAL", bk_csv: "Tabele CSV", bk_csv_fav: "Favorite", bk_csv_hist: "Istoric", bk_csv_ses: "Sesiuni", bk_exp: "Exportă copia de rezervă", bk_imp: "Importă copia de rezervă", bk_p_fav: "Favorite", bk_p_hist: "Istoric", bk_p_set: "Setări", bk_parts: "Conținutul copiei", bm_add: "Adaugă la favorite", bm_all: "Toate", bm_del: "Șterge", bm_dup: "Deja adăugat!", bm_empty: "Lista este goală.", bm_find: "Caută după nume…", bm_nomatch: "Niciun model nu se potrivește filtrului", bm_online: "Online", bm_online_t: "Arată doar online", bm_pin: "Fixează sus", bm_priv: "Priv", bm_title: "Favorite", bm_unpin: "Anulează fixarea", bt_hints: "Sfaturi", bt_stats: "Spectatori", bt_vol: "Volum", cal_all_tag: "Toate etichetele", cal_allday: "Toată ziua", cal_c_dur: "Durată", cal_c_model: "Model", cal_c_n: "Vizite", cal_c_time: "Intrare", cal_hist: "Istoric", cal_loaded: "Program încărcat: {a} din {b}", cal_models: "Modele", cal_no_sched: "Programul este preluat din profilurile favoritelor. Deschide Favorite și așteaptă câteva secunde.", cal_none: "Nimic în această zi", cal_reset: "Resetează", cal_sched: "Program", cal_title: "Calendar", cal_today: "Azi", cal_total: "Total", cal_watched: "Văzut", cf_cancel: "Anulează", cf_ok: "Da", ch_avg: "Media zilnică", ch_btn: "Grafice", ch_days: "Timp pe zi, 14 zile", ch_hours: "Activitate pe ore", ch_peak: "Vârf", ch_top: "Top după timp", copy: "LINK", dark: "Temă închisă / deschisă", don_copied: "Adresă copiată", don_fail: "Copierea a eșuat", don_sub: "Mulțumesc că folosești scriptul", don_tap: "Atinge o adresă ca să o vezi completă. Butonul din dreapta o copiază.", don_tip: "Susține autorul", don_title: "Cumpără-i autorului o cafea", drag: "⁞⁞ COMENZI", err: "EROARE", favs: "FAVORITE", fr_hold: "Ține apăsat pentru cadrul curent", fr_none: "Fără cadru: modelul este offline", fs: "ECRAN", grp_all: "Toate grupurile", grp_new: "+ Grup nou…", grp_none: "Fără grup", hd_all: "Ascunde tot", hd_cats: "Categorii", hd_clear: "Șterge toate regulile mele", hd_details: "Detalii", hd_g_chat: "Chat", hd_g_site: "Site", hd_g_win: "Ferestre pop-up", hd_hidden: "Ascunse {n} din {m}", hd_na: "încă negăsit", hd_none: "Încă nu există reguli.", hd_off: "Arată tot", hd_p_clean: "Player curat", hd_p_min: "Minim", hd_peek: "Ține apăsat un element ca să vezi ce ascunde", hd_pick: "Alege element", hd_reset: "Resetează pentru acest site", hd_rules: "Regulile mele", hd_title: "Ascundere", hide: "Ascunde (H)", hints: "M=SUNET F=ECRAN T=TEATRU H=ASCUNDE", hist_clear: "Șterge", hist_clear_q: "Ștergi tot istoricul de vizionări?", hist_del: "Elimină din istoric", hist_empty: "Gol deocamdată. Timpul se numără cât timp rulează videoul.", hist_s_last: "Recente", hist_s_time: "Timp", hist_s_vis: "Vizite", hist_title: "Istoric", hist_total: "Modele: {n} · Total: {t}", hist_visits: "vizite", home: "Acasă", lang_auto: "Auto", lang_title: "Limbă", min: "Minimizează", mv_abr_row: "Calitate automată", mv_abr_t: "Calitate automată: scade calitatea când video se blochează sau rețeaua e slabă și o crește când conexiunea e stabilă", mv_add: "Adaugă", mv_auto: "Auto", mv_auto_all: "Adaugă automat toate fluxurile live", mv_back: "Înapoi", mv_bg: "Redare în fundal", mv_bg_t: "Fluxurile continuă să ruleze când schimbi fila, închizi această fereastră sau ieși din browser", mv_bm_h: "Favorite în direct", mv_clear_q: "Închizi toate fluxurile?", mv_close: "Închide", mv_cols: "Aspect", mv_cur: "Fluxul curent", mv_dvr: "Buffer de derulare înapoi", mv_dvr_off: "Oprit", mv_fs: "Ecran complet", mv_full: "Fereastra este plină", mv_go: "Adaugă", mv_hint: "Adaugă fluxuri ca să urmărești mai multe deodată. Atinge o casetă ca să muți sunetul pe ea.", mv_link_h: "Sau prin link", mv_load: "Se încarcă…", mv_min_u: "min", mv_mute: "Fără sunet", mv_none: "Nu există favorite în direct", mv_off: "Flux indisponibil", mv_pip: "Imagine în imagine", mv_play: "Redare / pauză", mv_qual: "Calitate", mv_retry: "atinge pentru a reîncerca", mv_search: "Caută în favoritele în direct", mv_title: "Player", no_vid: "FĂRĂ VIDEO", note_ph: "Notă... (#taguri)", pip: "PIP", pk_cancel: "Anulează", pk_found: "potriviri", pk_hint: "Atinge un element de pe pagină", pk_ok: "Ascunde", pk_up: "Mai sus", qual: "CALIT.", rad_media: "Media", rad_nav: "Favorite", rad_next: "Urm.", rad_page: "Pagină", rad_prev: "Ant.", rad_reset: "Resetează poziția butonului", rad_title: "Meniu radial", rad_vdn: "Mai încet", rad_view: "Ecran", rad_vup: "Mai tare", rec: "ÎNREG.", rec_tip: "Atingere: înregistrează · Apăsare lungă: setări", rel: "Reîncarcă", rs_cur: "Fluxul curent:", rs_gbh: "GB/h", rs_hint: "Când o parte atinge dimensiunea aleasă, este salvată ca fișier separat, iar înregistrarea continuă într-unul nou. Datele se scriu pe disc în blocuri mici; dacă discul nu este disponibil, rămân în memoria telefonului, așa că alege părți mai mici.", rs_mbmin: "MB/min", rs_next: "Modificările se aplică de la următoarea parte sau de la o înregistrare nouă.", rs_now: "Acum", rs_off: "Oprit", rs_p: "parte", rs_part: "Mărimea părții (fișierul se împarte)", rs_q_hi: "Înaltă", rs_q_low: "Scăzută", rs_q_mid: "Medie", rs_q_min: "Economie", rs_quality: "Calitate (bitrate)", rs_recover: "Recuperează înregistrările nesalvate", rs_resnote: "Captura înregistrează la rezoluția fluxului: opțiunile de mai sus schimbă doar bitrate-ul (compresia), iar 1080p–360p arată calitatea estimată a imaginii. În modul HLS se alege un flux cu înălțimea de cel mult cea indicată.", rs_src: "Sursă", rs_src_cap: "Captură (recodare)", rs_src_hls: "HLS (fără recodare)", rs_src_note: "HLS descarcă segmentele fluxului ca atare, fără recodare, și salvează MP4. Are nevoie de adresa fluxului; dacă nu este disponibilă, se folosește captura. Calitatea este cea originală, ca în fluxul sursă. Înregistrarea se scrie pe disc în mici puncte de control.", rs_t: "total", rs_title: "Setări de înregistrare", rs_total: "Oprește la mărimea totală", rs_upto: "până la", search: "CAUTĂ", seen_now: "chiar acum", seen_tip: "Ultima oară online", settings: "Setări", sleep_custom: "Timp personalizat", sleep_hint: "La expirarea timpului, playerul se oprește, iar orice înregistrare se oprește și se salvează. Atinge ecranul negru pentru a-l readuce.", sleep_left: "Rămas", sleep_sec: "sec", sleep_start: "Pornește", sleep_title: "Temporizator de somn", sleep_wake: "Temporizatorul s-a încheiat.\nAtinge pentru a continua", snap: "FOTO", sort_date: "Data", sort_last: "Văzut", sort_live: "Live", sort_name: "Nume", sort_viewers: "Spectatori", sound: "SUNET", st_av_refresh: "Actualizează avatarele", st_away: "BRB", st_back: "Înapoi", st_btns: "Butoanele panoului", st_btns_hint: "Atinge pentru a ascunde un buton sau a-l readuce.", st_btns_reset: "Arată toate butoanele", st_color: "Culoarea panoului", st_color_rnd: "Aleatorie", st_cols: "Coloane", st_custom: "Culoare personalizată", st_data: "Date", st_fav: "Favorite", st_favnav: "Butoane „înapoi / înainte” pe telecomandă", st_group: "Grup", st_hide_grp: "Ascunde selectorul de grup", st_hide_note: "Ascunde câmpul de notițe (etichete)", st_hide_topic: "Ascunde descrierea modelului", st_hist: "Istoric vizionări", st_hist_on: "Păstrează istoricul", st_hue: "Nuanță", st_lang: "Limbă", st_lig: "Luminozitate", st_list: "Listă", st_look: "Aspect", st_open: "Deschide", st_prev: "Previzualizări ale modelelor", st_prev_off: "Fără actualizare", st_prev_spd: "Reîmprospătarea previzualizărilor", st_private: "Privat", st_ptags: "Ascunde etichetele profilului (BongaCams)", st_rec: "Înregistrare video", st_sat: "Saturație", st_scale: "Dimensiunea panoului", st_site_dark: "Tema întunecată a site-ului", st_star: "Stea pe miniaturi", st_vip: "VIP", star_add: "Adaugă la favorite", star_del: "Elimină din favorite", star_del_q: "Acest model are o notă sau un grup. Îl elimini din favorite?", thtr: "TEATRU", tp_less: "[Mai puțin]", tp_more: "[Mai mult]", u_d: "z", u_gb: "GB", u_h: "h", u_m: "min", u_mb: "MB", viewers_tip: "Spectatori în cameră", w_new: "Nevăzut", w_seen: "Văzut", }, HU: { ago: "{n} ezelőtt", bg: "HÁTTÉR", bk_csv: "CSV-táblázatok", bk_csv_fav: "Kedvencek", bk_csv_hist: "Előzmények", bk_csv_ses: "Munkamenetek", bk_exp: "Biztonsági mentés exportálása", bk_imp: "Biztonsági mentés importálása", bk_p_fav: "Kedvencek", bk_p_hist: "Előzmények", bk_p_set: "Beállítások", bk_parts: "A mentés tartalma", bm_add: "Hozzáadás a kedvencekhez", bm_all: "Mind", bm_del: "Törlés", bm_dup: "Már hozzá van adva!", bm_empty: "A lista üres.", bm_find: "Keresés név szerint…", bm_nomatch: "Nincs a szűrőnek megfelelő modell", bm_online: "Online", bm_online_t: "Csak az online-ok", bm_pin: "Kitűzés felülre", bm_priv: "Priv", bm_title: "Kedvencek", bm_unpin: "Kitűzés megszüntetése", bt_hints: "Tippek", bt_stats: "Nézők", bt_vol: "Hangerő", cal_all_tag: "Minden címke", cal_allday: "Egész nap", cal_c_dur: "Idő", cal_c_model: "Modell", cal_c_n: "Alkalom", cal_c_time: "Belépés", cal_hist: "Előzmények", cal_loaded: "Menetrend betöltve: {a} / {b}", cal_models: "Modellek", cal_no_sched: "A menetrend a kedvencek profiljából töltődik be. Nyisd meg a Kedvenceket, és várj pár másodpercet.", cal_none: "Ezen a napon nincs semmi", cal_reset: "Visszaállítás", cal_sched: "Menetrend", cal_title: "Naptár", cal_today: "Ma", cal_total: "Összesen", cal_watched: "Nézett", cf_cancel: "Mégse", cf_ok: "Igen", ch_avg: "Napi átlag", ch_btn: "Diagramok", ch_days: "Idő naponta, 14 nap", ch_hours: "Aktivitás óránként", ch_peak: "Csúcs", ch_top: "Toplista idő szerint", copy: "LINK", dark: "Sötét / világos téma", don_copied: "Cím másolva", don_fail: "A másolás nem sikerült", don_sub: "Köszönöm, hogy használod", don_tap: "Érintsd meg a címet a teljes megjelenítéshez. A jobb oldali gomb másolja.", don_tip: "A szerző támogatása", don_title: "Fizess egy kávét a szerzőnek", drag: "⁞⁞ VEZÉRLŐ", err: "HIBA", favs: "KEDVENCEK", fr_hold: "Tartsd nyomva az aktuális képkockához", fr_none: "Nincs kép: a modell offline", fs: "TELJES", grp_all: "Minden csoport", grp_new: "+ Új csoport…", grp_none: "Nincs csoport", hd_all: "Mindent elrejt", hd_cats: "Kategóriák", hd_clear: "Összes saját szabály törlése", hd_details: "Részletek", hd_g_chat: "Csevegés", hd_g_site: "Oldal", hd_g_win: "Felugró ablakok", hd_hidden: "Elrejtve: {n} / {m}", hd_na: "még nem található", hd_none: "Még nincsenek szabályok.", hd_off: "Mindent mutat", hd_p_clean: "Tiszta lejátszó", hd_p_min: "Minimum", hd_peek: "Tartsd nyomva a tételt, hogy lásd, mit rejt el", hd_pick: "Elem kiválasztása", hd_reset: "Visszaállítás ezen az oldalon", hd_rules: "Saját szabályok", hd_title: "Elrejtés", hide: "Elrejtés (H)", hints: "M=HANG F=TELJES T=MOZI H=ELREJT", hist_clear: "Törlés", hist_clear_q: "Törlöd a teljes megtekintési előzményt?", hist_del: "Eltávolítás az előzményekből", hist_empty: "Egyelőre üres. Az idő a videó lejátszása közben telik.", hist_s_last: "Legutóbbi", hist_s_time: "Idő", hist_s_vis: "Látogatások", hist_title: "Előzmények", hist_total: "Modellek: {n} · Összesen: {t}", hist_visits: "látogatás", home: "Kezdőlap", lang_auto: "Auto", lang_title: "Nyelv", min: "Kicsinyítés", mv_abr_row: "Automatikus minőség", mv_abr_t: "Automatikus minőség: csökkenti a minőséget akadozásnál vagy gyenge hálózatnál, és növeli, ha a kapcsolat stabil", mv_add: "Hozzáadás", mv_auto: "Auto", mv_auto_all: "Az összes élő adás automatikus hozzáadása", mv_back: "Vissza", mv_bg: "Lejátszás a háttérben", mv_bg_t: "Az adások tovább szólnak, ha lapot váltasz, bezárod ezt az ablakot vagy kilépsz a böngészőből", mv_bm_h: "Élő könyvjelzők", mv_clear_q: "Bezárod az összes adást?", mv_close: "Bezárás", mv_cols: "Elrendezés", mv_cur: "Jelenlegi adás", mv_dvr: "Visszatekerési puffer", mv_dvr_off: "Ki", mv_fs: "Teljes képernyő", mv_full: "Az ablak megtelt", mv_go: "Hozzáadás", mv_hint: "Adj hozzá adásokat, hogy többet nézhess egyszerre. Érints meg egy csempét, hogy átvidd rá a hangot.", mv_link_h: "Vagy hivatkozással", mv_load: "Betöltés…", mv_min_u: "perc", mv_mute: "Némítás", mv_none: "Nincs élő könyvjelző", mv_off: "Az adás nem érhető el", mv_pip: "Kép a képben", mv_play: "Lejátszás / szünet", mv_qual: "Minőség", mv_retry: "érintsd meg az újrapróbáláshoz", mv_search: "Keresés az élő könyvjelzők között", mv_title: "Lejátszó", no_vid: "NINCS VIDEÓ", note_ph: "Jegyzet... (#címkék)", pip: "PIP", pk_cancel: "Mégse", pk_found: "találat", pk_hint: "Érints meg egy elemet az oldalon", pk_ok: "Elrejtés", pk_up: "Feljebb", qual: "MIN.", rad_media: "Média", rad_nav: "Kedvencek", rad_next: "Következő", rad_page: "Oldal", rad_prev: "Előző", rad_reset: "Gomb helyének visszaállítása", rad_title: "Körmenü", rad_vdn: "Halkabb", rad_view: "Nézet", rad_vup: "Hangosabb", rec: "FELV.", rec_tip: "Érintés: felvétel · Nyomva tartás: beállítások", rel: "Újratöltés", rs_cur: "Jelenlegi adás:", rs_gbh: "GB/ó", rs_hint: "Amikor egy rész eléri a kiválasztott méretet, külön fájlként mentődik, a felvétel pedig egy újban folytatódik. Az adatok kis blokkokban íródnak a lemezre; ha a lemez nem érhető el, a telefon memóriájában maradnak, ilyenkor érdemes kisebb részeket választani.", rs_mbmin: "MB/perc", rs_next: "A módosítások a következő résztől vagy új felvételtől érvényesek.", rs_now: "Most", rs_off: "Ki", rs_p: "rész", rs_part: "Részméret (a fájl darabolódik)", rs_q_hi: "Magas", rs_q_low: "Alacsony", rs_q_mid: "Közepes", rs_q_min: "Takarékos", rs_quality: "Minőség (bitráta)", rs_recover: "Nem mentett felvételek helyreállítása", rs_resnote: "A Capture mód az adás felbontásában rögzít: a fenti beállítások csak a bitrátát (tömörítést) módosítják, az 1080p–360p pedig a várható képminőséget mutatja. HLS módban a megadott magasságot meg nem haladó adást választja a rendszer.", rs_src: "Forrás", rs_src_cap: "Rögzítés (újrakódolás)", rs_src_hls: "HLS (újrakódolás nélkül)", rs_src_note: "A HLS az adás szegmenseit úgy tölti le, ahogy vannak, újrakódolás nélkül, és MP4-be menti. Szükség van az adás címére; ha nem érhető el, a rögzítés lesz használva. A minőség eredeti, megegyezik a forrásadáséval. A felvétel kis ellenőrzőpontokban íródik lemezre.", rs_t: "összes", rs_title: "Felvétel beállításai", rs_total: "Leállítás összméretnél", rs_upto: "legfeljebb", search: "KERESÉS", seen_now: "épp most", seen_tip: "Utoljára online", settings: "Beállítások", sleep_custom: "Egyéni idő", sleep_hint: "Az idő leteltével a lejátszó kikapcsol, a felvétel pedig leáll és mentésre kerül. Koppints a fekete képernyőre a visszakapcsoláshoz.", sleep_left: "Hátra van", sleep_sec: "mp", sleep_start: "Indítás", sleep_title: "Alvásidőzítő", sleep_wake: "Az időzítő lejárt.\nKoppints a folytatáshoz", snap: "FOTÓ", sort_date: "Dátum", sort_last: "Látva", sort_live: "Live", sort_name: "Név", sort_viewers: "Nézők", sound: "HANG", st_av_refresh: "Avatarok frissítése", st_away: "BRB", st_back: "Vissza", st_btns: "Panelgombok", st_btns_hint: "Koppints egy gomb elrejtéséhez vagy visszaállításához.", st_btns_reset: "Összes gomb megjelenítése", st_color: "Panel színe", st_color_rnd: "Véletlen", st_cols: "Oszlopok", st_custom: "Egyéni szín", st_data: "Adatok", st_fav: "Kedvencek", st_favnav: "„Vissza / előre” gombok a vezérlőn", st_group: "Csoport", st_hide_grp: "Csoportválasztó elrejtése", st_hide_note: "Megjegyzés mező elrejtése (címkék)", st_hide_topic: "Modell leírásának elrejtése", st_hist: "Megtekintési előzmények", st_hist_on: "Előzmények mentése", st_hue: "Árnyalat", st_lang: "Nyelv", st_lig: "Fényerő", st_list: "Lista", st_look: "Megjelenés", st_open: "Megnyitás", st_prev: "Modellek előnézete", st_prev_off: "Nincs frissítés", st_prev_spd: "Előnézet frissítése", st_private: "Privát", st_ptags: "Profilcímkék elrejtése (BongaCams)", st_rec: "Videófelvétel", st_sat: "Telítettség", st_scale: "Panel mérete", st_site_dark: "Az oldal sötét témája", st_star: "Csillag az előnézeteken", st_vip: "VIP", star_add: "Hozzáadás a kedvencekhez", star_del: "Eltávolítás a kedvencekből", star_del_q: "Ennek a modellnek van jegyzete vagy csoportja. Eltávolítod a kedvencekből?", thtr: "MOZI", tp_less: "[Kevesebb]", tp_more: "[Több]", u_d: "n", u_gb: "GB", u_h: "ó", u_m: "perc", u_mb: "MB", viewers_tip: "Nézők a szobában", w_new: "Nem nézett", w_seen: "Nézett", }, BG: { ago: "преди {n}", bg: "ФОН", bk_csv: "CSV таблици", bk_csv_fav: "Любими", bk_csv_hist: "История", bk_csv_ses: "Сесии", bk_exp: "Експорт на резервно копие", bk_imp: "Импорт на резервно копие", bk_p_fav: "Любими", bk_p_hist: "История", bk_p_set: "Настройки", bk_parts: "Съдържание на копието", bm_add: "Добави в любими", bm_all: "Всички", bm_del: "Изтрий", bm_dup: "Вече е добавен!", bm_empty: "Списъкът е празен.", bm_find: "Търсене по име…", bm_nomatch: "Няма модели по филтъра", bm_online: "Онлайн", bm_online_t: "Само онлайн", bm_pin: "Закрепи най-горе", bm_priv: "Прив", bm_title: "Любими", bm_unpin: "Откачи", bt_hints: "Подсказки", bt_stats: "Зрители", bt_vol: "Сила на звука", cal_all_tag: "Всички тагове", cal_allday: "Цял ден", cal_c_dur: "Прод.", cal_c_model: "Модел", cal_c_n: "Пъти", cal_c_time: "Вход", cal_hist: "История", cal_loaded: "Разписание заредено: {a} от {b}", cal_models: "Модели", cal_no_sched: "Разписанието се взема от профилите на любимите. Отворете „Любими“ и изчакайте няколко секунди.", cal_none: "Няма нищо за този ден", cal_reset: "Нулиране", cal_sched: "Разписание", cal_title: "Календар", cal_today: "Днес", cal_total: "Общо", cal_watched: "Гледан", cf_cancel: "Отказ", cf_ok: "Да", ch_avg: "Средно на ден", ch_btn: "Диаграми", ch_days: "Време по дни, 14 дни", ch_hours: "Активност по часове", ch_peak: "Пик", ch_top: "Топ по време", copy: "ЛИНК", dark: "Тъмна / светла тема", don_copied: "Адресът е копиран", don_fail: "Неуспешно копиране", don_sub: "Благодаря, че го ползвате", don_tap: "Докоснете адреса, за да го видите изцяло. Бутонът вдясно го копира.", don_tip: "Подкрепи автора", don_title: "Почерпи автора с кафе", drag: "⁞⁞ ПУЛТ", err: "ГРЕШКА", favs: "ЛЮБИМИ", fr_hold: "Задръжте, за да видите текущия кадър", fr_none: "Няма кадър: моделът е офлайн", fs: "ЕКРАН", grp_all: "Всички групи", grp_new: "+ Нова група…", grp_none: "Без група", hd_all: "Скрий всичко", hd_cats: "Категории", hd_clear: "Изтрий всички мои правила", hd_details: "Подробности", hd_g_chat: "Чат", hd_g_site: "Сайт", hd_g_win: "Изскачащи прозорци", hd_hidden: "Скрити {n} от {m}", hd_na: "още не е намерено", hd_none: "Все още няма правила.", hd_off: "Покажи всичко", hd_p_clean: "Чист плеър", hd_p_min: "Минимум", hd_peek: "Задръжте елемент, за да видите какво скрива", hd_pick: "Избор на елемент", hd_reset: "Нулиране за този сайт", hd_rules: "Моите правила", hd_title: "Скриване", hide: "Скриване (H)", hints: "M=ЗВУК F=ЕКРАН T=ТЕАТЪР H=СКРИЙ", hist_clear: "Изчисти", hist_clear_q: "Да се изтрие ли цялата история на гледане?", hist_del: "Премахни от историята", hist_empty: "Засега е празно. Времето се брои, докато видеото върви.", hist_s_last: "Скорошни", hist_s_time: "Време", hist_s_vis: "Посещения", hist_title: "История", hist_total: "Модели: {n} · Общо: {t}", hist_visits: "посещ.", home: "Начало", lang_auto: "Авто", lang_title: "Език", min: "Свиване", mv_abr_row: "Автоматично качество", mv_abr_t: "Автоматично качество: намалява качеството при засичане или слаба мрежа и го повишава, когато връзката е стабилна", mv_add: "Добави", mv_auto: "Авто", mv_auto_all: "Добавяй всички потоци на живо автоматично", mv_back: "Назад", mv_bg: "Възпроизвеждане във фонов режим", mv_bg_t: "Потоците продължават да се възпроизвеждат, когато смените раздела, затворите този прозорец или излезете от браузъра", mv_bm_h: "Любими на живо", mv_clear_q: "Да се затворят ли всички потоци?", mv_close: "Затвори", mv_cols: "Подредба", mv_cur: "Текущ поток", mv_dvr: "Буфер за превъртане", mv_dvr_off: "Изкл.", mv_fs: "Цял екран", mv_full: "Прозорецът е пълен", mv_go: "Добави", mv_hint: "Добавете потоци, за да гледате няколко наведнъж. Докоснете плочка, за да прехвърлите звука към нея.", mv_link_h: "Или чрез линк", mv_load: "Зареждане…", mv_min_u: "мин", mv_mute: "Без звук", mv_none: "Няма любими на живо", mv_off: "Потокът не е наличен", mv_pip: "Картина в картина", mv_play: "Пускане / пауза", mv_qual: "Качество", mv_retry: "докоснете за повторен опит", mv_search: "Търсене в любимите на живо", mv_title: "Плеър", no_vid: "НЯМА ВИДЕО", note_ph: "Бележка... (#тагове)", pip: "PIP", pk_cancel: "Отказ", pk_found: "съвп.", pk_hint: "Докоснете елемент на страницата", pk_ok: "Скрий", pk_up: "По-горе", qual: "КАЧ-ВО", rad_media: "Медия", rad_nav: "Любими", rad_next: "След.", rad_page: "Страница", rad_prev: "Пред.", rad_reset: "Нулиране на позицията на бутона", rad_title: "Радиално меню", rad_vdn: "По-тихо", rad_view: "Екран", rad_vup: "По-силно", rec: "ЗАПИС", rec_tip: "Докосване: запис · Задържане: настройки", rel: "Рестарт", rs_cur: "Текущ поток:", rs_gbh: "ГБ/ч", rs_hint: "Когато една част достигне избрания размер, тя се запазва като отделен файл, а записът продължава в нов. Данните се пишат на диска на малки блокове; ако дискът не е достъпен, остават в паметта на телефона, затова изберете по-малки части.", rs_mbmin: "МБ/мин", rs_next: "Промените важат от следващата част или от нов запис.", rs_now: "Сега", rs_off: "Изкл.", rs_p: "част", rs_part: "Размер на част (файлът се дели)", rs_q_hi: "Високо", rs_q_low: "Ниско", rs_q_mid: "Средно", rs_q_min: "Икономия", rs_quality: "Качество (битрейт)", rs_recover: "Възстанови незапазените записи", rs_resnote: "Захватът записва в резолюцията на потока: опциите по-горе променят само битрейта (компресията), а 1080p–360p показват очакваното качество на картината. В режим HLS се избира поток с височина не по-голяма от зададената.", rs_src: "Източник", rs_src_cap: "Заснемане (прекодиране)", rs_src_hls: "HLS (без прекодиране)", rs_src_note: "HLS изтегля сегментите на потока такива, каквито са, без прекодиране, и записва MP4. Нужен е адресът на потока; ако няма, се използва заснемане. Качеството е оригинално, като на изходния поток. Записът се пише на диска на малки части.", rs_t: "общо", rs_title: "Настройки на записа", rs_total: "Спри при общ размер", rs_upto: "до", search: "ТЪРСЕНЕ", seen_now: "току-що", seen_tip: "Последно онлайн", settings: "Настройки", sleep_custom: "Собствено време", sleep_hint: "Когато времето изтече, плеърът се изключва, а всеки запис спира и се запазва. Докоснете черния екран, за да го върнете.", sleep_left: "Остава", sleep_sec: "сек", sleep_start: "Старт", sleep_title: "Таймер за заспиване", sleep_wake: "Таймерът приключи.\nДокоснете, за да продължите", snap: "СНИМКА", sort_date: "Дата", sort_last: "Видяна", sort_live: "Лайв", sort_name: "Име", sort_viewers: "Зрители", sound: "ЗВУК", st_av_refresh: "Обнови аватарите", st_away: "BRB", st_back: "Назад", st_btns: "Бутони на панела", st_btns_hint: "Докоснете, за да скриете бутон или да го върнете.", st_btns_reset: "Покажи всички бутони", st_color: "Цвят на панела", st_color_rnd: "Случаен", st_cols: "Колони", st_custom: "Собствен цвят", st_data: "Данни", st_fav: "Любими", st_favnav: "Бутони «назад / напред» на пулта", st_group: "Група", st_hide_grp: "Скриване на избора на група", st_hide_note: "Скрий полето за бележка (тагове)", st_hide_topic: "Скриване на описанието на модела", st_hist: "История на гледането", st_hist_on: "Пази история", st_hue: "Нюанс", st_lang: "Език", st_lig: "Яркост", st_list: "Списък", st_look: "Оформление", st_open: "Отвори", st_prev: "Превюта на моделите", st_prev_off: "Без обновяване", st_prev_spd: "Обновяване на превютата", st_private: "Приват", st_ptags: "Скрий таговете на профила (BongaCams)", st_rec: "Запис на видео", st_sat: "Наситеност", st_scale: "Размер на панела", st_site_dark: "Тъмна тема на сайта", st_star: "Звезда върху миниатюрите", st_vip: "VIP", star_add: "Добави в любими", star_del: "Премахни от любими", star_del_q: "Този модел има бележка или група. Да се премахне ли от любими?", thtr: "ТЕАТЪР", tp_less: "[По-малко]", tp_more: "[Още]", u_d: "д", u_gb: "ГБ", u_h: "ч", u_m: "мин", u_mb: "МБ", viewers_tip: "Зрители в стаята", w_new: "Негледан", w_seen: "Гледан", }, EL: { ago: "πριν {n}", bg: "ΦΟΝΤΟ", bk_csv: "Πίνακες CSV", bk_csv_fav: "Αγαπημένα", bk_csv_hist: "Ιστορικό", bk_csv_ses: "Συνεδρίες", bk_exp: "Εξαγωγή αντιγράφου ασφαλείας", bk_imp: "Εισαγωγή αντιγράφου ασφαλείας", bk_p_fav: "Αγαπημένα", bk_p_hist: "Ιστορικό", bk_p_set: "Ρυθμίσεις", bk_parts: "Περιεχόμενο αντιγράφου", bm_add: "Προσθήκη στα αγαπημένα", bm_all: "Όλα", bm_del: "Διαγραφή", bm_dup: "Έχει ήδη προστεθεί!", bm_empty: "Η λίστα είναι άδεια.", bm_find: "Αναζήτηση με όνομα…", bm_nomatch: "Κανένα μοντέλο δεν ταιριάζει με το φίλτρο", bm_online: "Online", bm_online_t: "Μόνο online", bm_pin: "Καρφίτσωμα στην κορυφή", bm_priv: "Priv", bm_title: "Αγαπημένα", bm_unpin: "Ξεκαρφίτσωμα", bt_hints: "Συμβουλές", bt_stats: "Θεατές", bt_vol: "Ένταση", cal_all_tag: "Όλες οι ετικέτες", cal_allday: "Όλη μέρα", cal_c_dur: "Διάρκ.", cal_c_model: "Μοντέλο", cal_c_n: "Φορές", cal_c_time: "Είσοδος", cal_hist: "Ιστορικό", cal_loaded: "Πρόγραμμα φορτώθηκε: {a} από {b}", cal_models: "Μοντέλα", cal_no_sched: "Το πρόγραμμα λαμβάνεται από τα προφίλ των αγαπημένων. Ανοίξτε τα Αγαπημένα και περιμένετε λίγα δευτερόλεπτα.", cal_none: "Τίποτα αυτή την ημέρα", cal_reset: "Επαναφορά", cal_sched: "Πρόγραμμα", cal_title: "Ημερολόγιο", cal_today: "Σήμερα", cal_total: "Σύνολο", cal_watched: "Προβλήθηκε", cf_cancel: "Ακύρωση", cf_ok: "Ναι", ch_avg: "Ημερήσιος μέσος όρος", ch_btn: "Διαγράμματα", ch_days: "Χρόνος ανά ημέρα, 14 ημέρες", ch_hours: "Δραστηριότητα ανά ώρα", ch_peak: "Κορύφωση", ch_top: "Κορυφαία ανά χρόνο", copy: "LINK", dark: "Σκούρο / ανοιχτό θέμα", don_copied: "Η διεύθυνση αντιγράφηκε", don_fail: "Η αντιγραφή απέτυχε", don_sub: "Ευχαριστώ που το χρησιμοποιείτε", don_tap: "Αγγίξτε μια διεύθυνση για να τη δείτε ολόκληρη. Το κουμπί δεξιά την αντιγράφει.", don_tip: "Υποστήριξη του δημιουργού", don_title: "Κεράστε τον δημιουργό έναν καφέ", drag: "⁞⁞ ΧΕΙΡΙΣΤΗΡΙΟ", err: "ΣΦΑΛΜΑ", favs: "ΑΓΑΠΗΜΕΝΑ", fr_hold: "Πατήστε παρατεταμένα για το τρέχον καρέ", fr_none: "Δεν υπάρχει καρέ: το μοντέλο είναι εκτός σύνδεσης", fs: "ΠΛΗΡΗΣ", grp_all: "Όλες οι ομάδες", grp_new: "+ Νέα ομάδα…", grp_none: "Χωρίς ομάδα", hd_all: "Απόκρυψη όλων", hd_cats: "Κατηγορίες", hd_clear: "Διαγραφή όλων των κανόνων μου", hd_details: "Λεπτομέρειες", hd_g_chat: "Συνομιλία", hd_g_site: "Ιστότοπος", hd_g_win: "Αναδυόμενα παράθυρα", hd_hidden: "Κρυμμένα {n} από {m}", hd_na: "δεν βρέθηκε ακόμα", hd_none: "Δεν υπάρχουν κανόνες ακόμα.", hd_off: "Εμφάνιση όλων", hd_p_clean: "Καθαρός player", hd_p_min: "Ελάχιστο", hd_peek: "Κρατήστε πατημένο ένα στοιχείο για να δείτε τι κρύβει", hd_pick: "Επιλογή στοιχείου", hd_reset: "Επαναφορά για αυτόν τον ιστότοπο", hd_rules: "Οι κανόνες μου", hd_title: "Απόκρυψη", hide: "Απόκρυψη (H)", hints: "M=ΗΧΟΣ F=ΠΛΗΡΗΣ T=ΘΕΑΤΡΟ H=ΚΡΥΨΕ", hist_clear: "Εκκαθάριση", hist_clear_q: "Διαγραφή ολόκληρου του ιστορικού προβολών;", hist_del: "Αφαίρεση από το ιστορικό", hist_empty: "Άδειο προς το παρόν. Ο χρόνος μετριέται όσο παίζει το βίντεο.", hist_s_last: "Πρόσφατα", hist_s_time: "Χρόνος", hist_s_vis: "Επισκέψεις", hist_title: "Ιστορικό", hist_total: "Μοντέλα: {n} · Σύνολο: {t}", hist_visits: "επισκέψεις", home: "Αρχική", lang_auto: "Αυτόματο", lang_title: "Γλώσσα", min: "Ελαχιστοποίηση", mv_abr_row: "Αυτόματη ποιότητα", mv_abr_t: "Αυτόματη ποιότητα: μειώνει την ποιότητα όταν το βίντεο κολλάει ή το δίκτυο είναι αδύναμο και την αυξάνει όταν η σύνδεση είναι σταθερή", mv_add: "Προσθήκη", mv_auto: "Αυτόματο", mv_auto_all: "Αυτόματη προσθήκη όλων των ζωντανών ροών", mv_back: "Πίσω", mv_bg: "Αναπαραγωγή στο παρασκήνιο", mv_bg_t: "Οι ροές συνεχίζουν να παίζουν όταν αλλάζετε καρτέλα, κλείνετε αυτό το παράθυρο ή φεύγετε από το πρόγραμμα περιήγησης", mv_bm_h: "Αγαπημένα σε ζωντανή μετάδοση", mv_clear_q: "Κλείσιμο όλων των ροών;", mv_close: "Κλείσιμο", mv_cols: "Διάταξη", mv_cur: "Τρέχουσα ροή", mv_dvr: "Προσωρινή μνήμη επαναφοράς", mv_dvr_off: "Ανενεργό", mv_fs: "Πλήρης οθόνη", mv_full: "Το παράθυρο είναι γεμάτο", mv_go: "Προσθήκη", mv_hint: "Προσθέστε ροές για να βλέπετε πολλές ταυτόχρονα. Πατήστε ένα πλακίδιο για να μεταφέρετε τον ήχο σε αυτό.", mv_link_h: "Ή με σύνδεσμο", mv_load: "Φόρτωση…", mv_min_u: "λεπ.", mv_mute: "Σίγαση", mv_none: "Δεν υπάρχουν ζωντανά αγαπημένα", mv_off: "Η ροή δεν είναι διαθέσιμη", mv_pip: "Εικόνα σε εικόνα", mv_play: "Αναπαραγωγή / παύση", mv_qual: "Ποιότητα", mv_retry: "πατήστε για επανάληψη", mv_search: "Αναζήτηση στα ζωντανά αγαπημένα", mv_title: "Player", no_vid: "ΧΩΡΙΣ ΒΙΝΤΕΟ", note_ph: "Σημείωση... (#ετικέτες)", pip: "PIP", pk_cancel: "Ακύρωση", pk_found: "αντιστ.", pk_hint: "Αγγίξτε ένα στοιχείο της σελίδας", pk_ok: "Απόκρυψη", pk_up: "Πιο πάνω", qual: "ΠΟΙΟΤ.", rad_media: "Πολυμέσα", rad_nav: "Αγαπημένα", rad_next: "Επόμ.", rad_page: "Σελίδα", rad_prev: "Προηγ.", rad_reset: "Επαναφορά θέσης κουμπιού", rad_title: "Κυκλικό μενού", rad_vdn: "Πιο χαμηλά", rad_view: "Προβολή", rad_vup: "Πιο δυνατά", rec: "ΕΓΓΡ.", rec_tip: "Πάτημα: εγγραφή · Παρατεταμένο: ρυθμίσεις", rel: "Ανανέωση", rs_cur: "Τρέχον stream:", rs_gbh: "GB/ώρα", rs_hint: "Όταν ένα τμήμα φτάσει το επιλεγμένο μέγεθος, αποθηκεύεται ως ξεχωριστό αρχείο και η εγγραφή συνεχίζεται σε νέο. Τα δεδομένα γράφονται στον δίσκο σε μικρά κομμάτια· αν ο δίσκος δεν είναι διαθέσιμος, μένουν στη μνήμη του τηλεφώνου, οπότε επιλέξτε μικρότερα τμήματα.", rs_mbmin: "MB/λεπτό", rs_next: "Οι αλλαγές ισχύουν από το επόμενο τμήμα ή τη νέα εγγραφή.", rs_now: "Τώρα", rs_off: "Ανενεργό", rs_p: "τμήμα", rs_part: "Μέγεθος τμήματος (το αρχείο σπάει)", rs_q_hi: "Υψηλή", rs_q_low: "Χαμηλή", rs_q_mid: "Μεσαία", rs_q_min: "Οικονομία", rs_quality: "Ποιότητα (bitrate)", rs_recover: "Ανάκτηση μη αποθηκευμένων εγγραφών", rs_resnote: "Η λήψη (Capture) καταγράφει στην ανάλυση του stream: οι επιλογές παραπάνω αλλάζουν μόνο το bitrate (συμπίεση), ενώ τα 1080p–360p δείχνουν την αναμενόμενη ποιότητα εικόνας. Στη λειτουργία HLS επιλέγεται stream με ύψος όχι μεγαλύτερο από το καθορισμένο.", rs_src: "Πηγή", rs_src_cap: "Καταγραφή (επανακωδικοποίηση)", rs_src_hls: "HLS (χωρίς επανακωδικοποίηση)", rs_src_note: "Το HLS κατεβάζει τα τμήματα της ροής ως έχουν, χωρίς επανακωδικοποίηση, και αποθηκεύει MP4. Χρειάζεται η διεύθυνση της ροής· αν δεν είναι διαθέσιμη, χρησιμοποιείται η καταγραφή. Η ποιότητα είναι η αρχική, όπως στην πηγαία ροή. Η εγγραφή γράφεται στον δίσκο σε μικρά σημεία ελέγχου.", rs_t: "σύνολο", rs_title: "Ρυθμίσεις εγγραφής", rs_total: "Διακοπή στο συνολικό μέγεθος", rs_upto: "έως", search: "ΑΝΑΖΗΤ.", seen_now: "μόλις τώρα", seen_tip: "Τελευταία φορά online", settings: "Ρυθμίσεις", sleep_custom: "Προσαρμοσμένος χρόνος", sleep_hint: "Όταν λήξει ο χρόνος, ο player απενεργοποιείται και κάθε εγγραφή σταματά και αποθηκεύεται. Αγγίξτε τη μαύρη οθόνη για να τον επαναφέρετε.", sleep_left: "Απομένουν", sleep_sec: "δευτ.", sleep_start: "Έναρξη", sleep_title: "Χρονοδιακόπτης ύπνου", sleep_wake: "Ο χρονοδιακόπτης έληξε.\nΑγγίξτε για συνέχεια", snap: "ΦΩΤΟ", sort_date: "Ημ/νία", sort_last: "Τελευταία", sort_live: "Live", sort_name: "Όνομα", sort_viewers: "Θεατές", sound: "ΗΧΟΣ", st_av_refresh: "Ανανέωση avatar", st_away: "BRB", st_back: "Πίσω", st_btns: "Κουμπιά πάνελ", st_btns_hint: "Πατήστε για να κρύψετε ένα κουμπί ή να το επαναφέρετε.", st_btns_reset: "Εμφάνιση όλων των κουμπιών", st_color: "Χρώμα πάνελ", st_color_rnd: "Τυχαίο", st_cols: "Στήλες", st_custom: "Προσαρμοσμένο χρώμα", st_data: "Δεδομένα", st_fav: "Αγαπημένα", st_favnav: "Κουμπιά «προηγούμενο / επόμενο» στο χειριστήριο", st_group: "Ομάδα", st_hide_grp: "Απόκρυψη επιλογής ομάδας", st_hide_note: "Απόκρυψη πεδίου σημείωσης (ετικέτες)", st_hide_topic: "Απόκρυψη περιγραφής μοντέλου", st_hist: "Ιστορικό προβολών", st_hist_on: "Διατήρηση ιστορικού", st_hue: "Απόχρωση", st_lang: "Γλώσσα", st_lig: "Φωτεινότητα", st_list: "Λίστα", st_look: "Εμφάνιση", st_open: "Άνοιγμα", st_prev: "Προεπισκοπήσεις μοντέλων", st_prev_off: "Χωρίς ανανέωση", st_prev_spd: "Ανανέωση προεπισκόπησης", st_private: "Ιδιωτικό", st_ptags: "Απόκρυψη ετικετών προφίλ (BongaCams)", st_rec: "Εγγραφή βίντεο", st_sat: "Κορεσμός", st_scale: "Μέγεθος πάνελ", st_site_dark: "Σκούρο θέμα ιστοτόπου", st_star: "Αστέρι στις μικρογραφίες", st_vip: "VIP", star_add: "Προσθήκη στα αγαπημένα", star_del: "Αφαίρεση από τα αγαπημένα", star_del_q: "Αυτό το μοντέλο έχει σημείωση ή ομάδα. Αφαίρεση από τα αγαπημένα;", thtr: "ΘΕΑΤΡΟ", tp_less: "[Λιγότερα]", tp_more: "[Περισσότερα]", u_d: "μ", u_gb: "GB", u_h: "ώ", u_m: "λεπ.", u_mb: "MB", viewers_tip: "Θεατές στο δωμάτιο", w_new: "Μη προβεβλημένο", w_seen: "Προβλήθηκε", }, JA: { ago: "{n}前", bg: "背景", bk_csv: "CSV表", bk_csv_fav: "お気に入り", bk_csv_hist: "履歴", bk_csv_ses: "セッション", bk_exp: "バックアップをエクスポート", bk_imp: "バックアップをインポート", bk_p_fav: "お気に入り", bk_p_hist: "履歴", bk_p_set: "設定", bk_parts: "バックアップの内容", bm_add: "お気に入りに追加", bm_all: "すべて", bm_del: "削除", bm_dup: "追加済みです!", bm_empty: "リストは空です。", bm_find: "名前で検索…", bm_nomatch: "条件に合うモデルがいません", bm_online: "オンライン", bm_online_t: "オンラインのみ表示", bm_pin: "先頭に固定", bm_priv: "プライベート", bm_title: "お気に入り", bm_unpin: "固定を解除", bt_hints: "ヒント", bt_stats: "視聴者", bt_vol: "音量", cal_all_tag: "すべてのタグ", cal_allday: "終日", cal_c_dur: "時間", cal_c_model: "モデル", cal_c_n: "回数", cal_c_time: "入室", cal_hist: "視聴履歴", cal_loaded: "スケジュール取得済み:{a}/{b}", cal_models: "モデル", cal_no_sched: "スケジュールはお気に入りのプロフィールから取得されます。お気に入りを開いて数秒お待ちください。", cal_none: "この日は何もありません", cal_reset: "リセット", cal_sched: "スケジュール", cal_title: "カレンダー", cal_today: "今日", cal_total: "合計", cal_watched: "視聴済み", cf_cancel: "キャンセル", cf_ok: "はい", ch_avg: "1日の平均", ch_btn: "グラフ", ch_days: "1日あたりの時間(14日間)", ch_hours: "時間帯別のアクティビティ", ch_peak: "ピーク", ch_top: "時間ランキング", copy: "リンク", dark: "ダーク / ライトテーマ", don_copied: "アドレスをコピーしました", don_fail: "コピーに失敗しました", don_sub: "ご利用ありがとうございます", don_tap: "アドレスをタップすると全体が表示されます。右のボタンでコピーできます。", don_tip: "作者を支援", don_title: "作者にコーヒーをおごる", drag: "⁞⁞ 操作パネル", err: "エラー", favs: "お気に入り", fr_hold: "長押しで現在のフレームを表示", fr_none: "フレームなし:モデルはオフラインです", fs: "全画面", grp_all: "すべてのグループ", grp_new: "+ 新しいグループ…", grp_none: "グループなし", hd_all: "すべて非表示", hd_cats: "カテゴリ", hd_clear: "マイルールをすべて削除", hd_details: "詳細", hd_g_chat: "チャット", hd_g_site: "サイト", hd_g_win: "ポップアップ", hd_hidden: "{m}件中{n}件を非表示", hd_na: "まだ見つかりません", hd_none: "ルールはまだありません。", hd_off: "すべて表示", hd_p_clean: "クリーンプレーヤー", hd_p_min: "最小", hd_peek: "項目を長押しすると、何を隠すか確認できます", hd_pick: "要素を選択", hd_reset: "このサイトをリセット", hd_rules: "マイルール", hd_title: "非表示設定", hide: "非表示 (H)", hints: "M=音声 F=全画面 T=シアター H=非表示", hist_clear: "消去", hist_clear_q: "視聴履歴をすべて削除しますか?", hist_del: "履歴から削除", hist_empty: "まだ空です。動画の再生中に時間がカウントされます。", hist_s_last: "最近", hist_s_time: "時間", hist_s_vis: "訪問", hist_title: "履歴", hist_total: "モデル: {n} · 合計: {t}", hist_visits: "回", home: "ホーム", lang_auto: "自動", lang_title: "言語", min: "最小化", mv_abr_row: "自動画質", mv_abr_t: "自動画質:映像がカクついたりネットワークが弱いときは画質を下げ、接続が安定したら上げます", mv_add: "追加", mv_auto: "自動", mv_auto_all: "ライブ配信をすべて自動で追加", mv_back: "戻る", mv_bg: "バックグラウンドで再生", mv_bg_t: "タブを切り替えたり、このウィンドウを閉じたり、ブラウザを離れても配信の再生が続きます", mv_bm_h: "ライブのブックマーク", mv_clear_q: "すべての配信を閉じますか?", mv_close: "閉じる", mv_cols: "レイアウト", mv_cur: "現在の配信", mv_dvr: "巻き戻しバッファ", mv_dvr_off: "オフ", mv_fs: "全画面", mv_full: "ウィンドウがいっぱいです", mv_go: "追加", mv_hint: "配信を追加すると複数を同時に視聴できます。タイルをタップすると、その配信に音声が切り替わります。", mv_link_h: "またはリンクで", mv_load: "読み込み中…", mv_min_u: "分", mv_mute: "ミュート", mv_none: "ライブのブックマークはありません", mv_off: "配信は利用できません", mv_pip: "ピクチャーインピクチャー", mv_play: "再生 / 一時停止", mv_qual: "画質", mv_retry: "タップして再試行", mv_search: "ライブのブックマークを検索", mv_title: "プレーヤー", no_vid: "動画なし", note_ph: "メモ... (#タグ)", pip: "PIP", pk_cancel: "キャンセル", pk_found: "件一致", pk_hint: "ページ上の要素をタップしてください", pk_ok: "非表示", pk_up: "上の階層", qual: "画質", rad_media: "メディア", rad_nav: "お気に入り", rad_next: "次へ", rad_page: "ページ", rad_prev: "前へ", rad_reset: "ボタン位置をリセット", rad_title: "ラジアルメニュー", rad_vdn: "音量ダウン", rad_view: "表示", rad_vup: "音量アップ", rec: "録画", rec_tip: "タップ: 録画 · 長押し: 設定", rel: "再読込", rs_cur: "現在のストリーム:", rs_gbh: "GB/時", rs_hint: "パートが選択したサイズに達すると、別のファイルとして保存され、録画は新しいパートで続きます。データは小さな単位でディスクに書き込まれ、ディスクが使えない場合はスマホのメモリに残るため、その場合はパートを小さめにしてください。", rs_mbmin: "MB/分", rs_next: "変更は次のパートまたは新しい録画から適用されます。", rs_now: "現在", rs_off: "オフ", rs_p: "パート", rs_part: "分割サイズ(ファイルを分割)", rs_q_hi: "高", rs_q_low: "低", rs_q_mid: "中", rs_q_min: "節約", rs_quality: "画質(ビットレート)", rs_recover: "未保存の録画を復元", rs_resnote: "キャプチャはストリームの解像度で録画します。上の設定で変わるのはビットレート(圧縮)だけで、1080p〜360pは期待できる画質の目安です。HLSモードでは、指定した高さ以下のストリームが選ばれます。", rs_src: "ソース", rs_src_cap: "キャプチャ(再エンコード)", rs_src_hls: "HLS(再エンコードなし)", rs_src_note: "HLSは、配信のセグメントを再エンコードせずそのままダウンロードし、MP4として保存します。配信のアドレスが必要で、利用できない場合はキャプチャが使われます。画質は元の配信と同じ、オリジナルのままです。録画は小さなチェックポイントごとにディスクへ書き込まれます。", rs_t: "合計", rs_title: "録画設定", rs_total: "合計サイズで停止", rs_upto: "最大", search: "検索", seen_now: "たった今", seen_tip: "最終オンライン", settings: "設定", sleep_custom: "時間を指定", sleep_hint: "時間になるとプレーヤーが停止し、録画中の場合は録画が止まって保存されます。黒い画面をタップすると元に戻ります。", sleep_left: "残り", sleep_sec: "秒", sleep_start: "開始", sleep_title: "スリープタイマー", sleep_wake: "スリープタイマーが終了しました。\nタップして続行", snap: "撮影", sort_date: "日付", sort_last: "最終", sort_live: "ライブ", sort_name: "名前", sort_viewers: "視聴者", sound: "音声", st_av_refresh: "アバターを更新", st_away: "BRB", st_back: "戻る", st_btns: "パネルのボタン", st_btns_hint: "タップしてボタンを非表示にしたり、元に戻したりできます。", st_btns_reset: "すべてのボタンを表示", st_color: "パネルの色", st_color_rnd: "ランダム", st_cols: "列数", st_custom: "カスタムカラー", st_data: "データ", st_fav: "お気に入り", st_favnav: "操作パネルの「前へ/次へ」ボタン", st_group: "グループ", st_hide_grp: "グループ選択を非表示", st_hide_note: "メモ欄(タグ)を非表示", st_hide_topic: "モデルの説明を非表示", st_hist: "視聴履歴", st_hist_on: "履歴を保存", st_hue: "色相", st_lang: "言語", st_lig: "明度", st_list: "リスト", st_look: "外観", st_open: "開く", st_prev: "モデルのプレビュー", st_prev_off: "更新しない", st_prev_spd: "プレビューの更新", st_private: "プライベート", st_ptags: "プロフィールのタグを非表示 (BongaCams)", st_rec: "動画の録画", st_sat: "彩度", st_scale: "パネルのサイズ", st_site_dark: "サイトのダークテーマ", st_star: "サムネイルの星", st_vip: "VIP", star_add: "お気に入りに追加", star_del: "お気に入りから削除", star_del_q: "このモデルにはメモまたはグループがあります。お気に入りから削除しますか?", thtr: "シアター", tp_less: "[閉じる]", tp_more: "[もっと見る]", u_d: "日", u_gb: "GB", u_h: "時間", u_m: "分", u_mb: "MB", viewers_tip: "ルームの視聴者数", w_new: "未視聴", w_seen: "視聴済み", }, ZH: { ago: "{n}前", bg: "背景", bk_csv: "CSV 表格", bk_csv_fav: "收藏夹", bk_csv_hist: "历史", bk_csv_ses: "会话", bk_exp: "导出备份", bk_imp: "导入备份", bk_p_fav: "收藏夹", bk_p_hist: "历史", bk_p_set: "设置", bk_parts: "备份内容", bm_add: "加入收藏夹", bm_all: "全部", bm_del: "删除", bm_dup: "已添加!", bm_empty: "列表为空。", bm_find: "按名称搜索…", bm_nomatch: "没有符合筛选条件的模特", bm_online: "在线", bm_online_t: "仅显示在线", bm_pin: "置顶", bm_priv: "私密", bm_title: "收藏夹", bm_unpin: "取消置顶", bt_hints: "提示", bt_stats: "观众", bt_vol: "音量", cal_all_tag: "所有标签", cal_allday: "全天", cal_c_dur: "时长", cal_c_model: "模特", cal_c_n: "次数", cal_c_time: "进入", cal_hist: "观看记录", cal_loaded: "已加载时间表:{a}/{b}", cal_models: "模特", cal_no_sched: "时间表来自收藏模特的个人资料。请打开“收藏”并稍等几秒。", cal_none: "这一天没有内容", cal_reset: "重置", cal_sched: "时间表", cal_title: "日历", cal_today: "今天", cal_total: "合计", cal_watched: "已观看", cf_cancel: "取消", cf_ok: "是", ch_avg: "日均", ch_btn: "图表", ch_days: "每日时长,14天", ch_hours: "按小时的活跃度", ch_peak: "峰值", ch_top: "时长排行", copy: "链接", dark: "深色 / 浅色主题", don_copied: "地址已复制", don_fail: "复制失败", don_sub: "感谢使用", don_tap: "点击地址可查看完整内容,右侧按钮可复制。", don_tip: "支持作者", don_title: "请作者喝杯咖啡", drag: "⁞⁞ 控制面板", err: "错误", favs: "收藏", fr_hold: "长按查看当前画面", fr_none: "无画面:模特已离线", fs: "全屏", grp_all: "所有分组", grp_new: "+ 新建分组…", grp_none: "无分组", hd_all: "全部隐藏", hd_cats: "分类", hd_clear: "删除我的所有规则", hd_details: "详情", hd_g_chat: "聊天", hd_g_site: "网站", hd_g_win: "弹窗", hd_hidden: "已隐藏 {n}/{m}", hd_na: "暂未找到", hd_none: "暂无规则。", hd_off: "全部显示", hd_p_clean: "纯净播放器", hd_p_min: "最简", hd_peek: "长按项目可查看其隐藏的内容", hd_pick: "选择元素", hd_reset: "重置此网站", hd_rules: "我的规则", hd_title: "隐藏设置", hide: "隐藏 (H)", hints: "M=声音 F=全屏 T=影院 H=隐藏", hist_clear: "清空", hist_clear_q: "删除全部观看历史?", hist_del: "从历史中移除", hist_empty: "暂无记录。视频播放时才会计时。", hist_s_last: "最近", hist_s_time: "时长", hist_s_vis: "访问", hist_title: "历史", hist_total: "模特:{n} · 总计:{t}", hist_visits: "次访问", home: "主页", lang_auto: "自动", lang_title: "语言", min: "最小化", mv_abr_row: "自动画质", mv_abr_t: "自动画质:画面卡顿或网络较弱时降低画质,连接稳定时提高画质", mv_add: "添加", mv_auto: "自动", mv_auto_all: "自动添加所有在线直播流", mv_back: "返回", mv_bg: "后台播放", mv_bg_t: "切换标签页、关闭此窗口或离开浏览器时,直播流会继续播放", mv_bm_h: "在线收藏", mv_clear_q: "要关闭所有直播流吗?", mv_close: "关闭", mv_cols: "布局", mv_cur: "当前直播流", mv_dvr: "回退缓冲", mv_dvr_off: "关闭", mv_fs: "全屏", mv_full: "窗口已满", mv_go: "添加", mv_hint: "添加直播流即可同时观看多个。点按某个画面,声音会切换到它。", mv_link_h: "或通过链接", mv_load: "加载中…", mv_min_u: "分钟", mv_mute: "静音", mv_none: "没有在线收藏", mv_off: "直播流不可用", mv_pip: "画中画", mv_play: "播放 / 暂停", mv_qual: "画质", mv_retry: "点按重试", mv_search: "搜索在线收藏", mv_title: "播放器", no_vid: "无视频", note_ph: "备注... (#标签)", pip: "PIP", pk_cancel: "取消", pk_found: "个匹配", pk_hint: "点击页面上的元素", pk_ok: "隐藏", pk_up: "上一级", qual: "画质", rad_media: "媒体", rad_nav: "收藏夹", rad_next: "下一个", rad_page: "页面", rad_prev: "上一个", rad_reset: "重置按钮位置", rad_title: "环形菜单", rad_vdn: "调小", rad_view: "显示", rad_vup: "调大", rec: "录制", rec_tip: "点按:录制 · 长按:设置", rel: "重新加载", rs_cur: "当前直播流:", rs_gbh: "GB/小时", rs_hint: "当某一段达到所选大小时,会保存为单独的文件,录制继续写入新的一段。数据会分小块写入磁盘;如果磁盘不可用,则保留在手机内存中,此时建议选择更小的分段。", rs_mbmin: "MB/分钟", rs_next: "更改将从下一段或新的录制开始生效。", rs_now: "当前", rs_off: "关闭", rs_p: "段", rs_part: "分段大小(文件会被拆分)", rs_q_hi: "高", rs_q_low: "低", rs_q_mid: "中", rs_q_min: "省空间", rs_quality: "画质(码率)", rs_recover: "恢复未保存的录制", rs_resnote: "捕获模式按直播流的分辨率录制:上面的选项只改变码率(压缩),1080p–360p 表示预期的画质。HLS 模式下会选择高度不超过所选值的直播流。", rs_src: "来源", rs_src_cap: "捕获(重新编码)", rs_src_hls: "HLS(不重新编码)", rs_src_note: "HLS 会原样下载直播流分段,不重新编码,并保存为 MP4。需要直播流地址;如果没有,则使用捕获。画质与源直播流相同,保持原始画质。录制内容会以小检查点写入磁盘。", rs_t: "总计", rs_title: "录制设置", rs_total: "达到总大小时停止", rs_upto: "最多", search: "搜索", seen_now: "刚刚", seen_tip: "最后在线", settings: "设置", sleep_custom: "自定义时间", sleep_hint: "时间到后,播放器会关闭,正在进行的录制会停止并保存。点按黑屏即可恢复播放器。", sleep_left: "剩余", sleep_sec: "秒", sleep_start: "开始", sleep_title: "睡眠定时器", sleep_wake: "睡眠定时已结束。\n点按继续", snap: "截图", sort_date: "日期", sort_last: "最近", sort_live: "直播", sort_name: "名称", sort_viewers: "观众", sound: "声音", st_av_refresh: "刷新头像", st_away: "BRB", st_back: "返回", st_btns: "面板按钮", st_btns_hint: "点按可隐藏按钮或将其恢复。", st_btns_reset: "显示所有按钮", st_color: "面板颜色", st_color_rnd: "随机", st_cols: "列数", st_custom: "自定义颜色", st_data: "数据", st_fav: "收藏夹", st_favnav: "控制面板上的“上一个 / 下一个”按钮", st_group: "群组", st_hide_grp: "隐藏分组选择", st_hide_note: "隐藏备注栏(标签)", st_hide_topic: "隐藏模特简介", st_hist: "观看历史", st_hist_on: "保存历史", st_hue: "色相", st_lang: "语言", st_lig: "亮度", st_list: "列表", st_look: "外观", st_open: "打开", st_prev: "模特预览", st_prev_off: "不刷新", st_prev_spd: "预览刷新", st_private: "私密", st_ptags: "隐藏个人资料标签 (BongaCams)", st_rec: "视频录制", st_sat: "饱和度", st_scale: "面板大小", st_site_dark: "网站深色主题", st_star: "缩略图上的星标", st_vip: "VIP", star_add: "加入收藏夹", star_del: "从收藏夹移除", star_del_q: "该模特有备注或分组。要从收藏夹移除吗?", thtr: "影院", tp_less: "[收起]", tp_more: "[展开]", u_d: "天", u_gb: "GB", u_h: "小时", u_m: "分钟", u_mb: "MB", viewers_tip: "房间内观众", w_new: "未观看", w_seen: "已观看", }, KO: { ago: "{n} 전", bg: "배경", bk_csv: "CSV 표", bk_csv_fav: "즐겨찾기", bk_csv_hist: "기록", bk_csv_ses: "세션", bk_exp: "백업 내보내기", bk_imp: "백업 가져오기", bk_p_fav: "즐겨찾기", bk_p_hist: "기록", bk_p_set: "설정", bk_parts: "백업 내용", bm_add: "즐겨찾기에 추가", bm_all: "전체", bm_del: "삭제", bm_dup: "이미 추가됨!", bm_empty: "목록이 비어 있습니다.", bm_find: "이름으로 검색…", bm_nomatch: "필터에 맞는 모델이 없습니다", bm_online: "온라인", bm_online_t: "온라인만 표시", bm_pin: "맨 위에 고정", bm_priv: "프라이빗", bm_title: "즐겨찾기", bm_unpin: "고정 해제", bt_hints: "도움말", bt_stats: "시청자", bt_vol: "볼륨", cal_all_tag: "모든 태그", cal_allday: "종일", cal_c_dur: "시간", cal_c_model: "모델", cal_c_n: "횟수", cal_c_time: "입장", cal_hist: "시청 기록", cal_loaded: "일정 불러옴: {a}/{b}", cal_models: "모델", cal_no_sched: "일정은 즐겨찾기 프로필에서 가져옵니다. 즐겨찾기를 열고 몇 초 기다려 주세요.", cal_none: "이날은 비어 있습니다", cal_reset: "초기화", cal_sched: "일정", cal_title: "캘린더", cal_today: "오늘", cal_total: "합계", cal_watched: "시청함", cf_cancel: "취소", cf_ok: "예", ch_avg: "일평균", ch_btn: "차트", ch_days: "일별 시간, 14일", ch_hours: "시간대별 활동", ch_peak: "최고치", ch_top: "시간 순위", copy: "링크", dark: "다크 / 라이트 테마", don_copied: "주소가 복사되었습니다", don_fail: "복사 실패", don_sub: "이용해 주셔서 감사합니다", don_tap: "주소를 탭하면 전체가 표시되고, 오른쪽 버튼으로 복사할 수 있습니다.", don_tip: "제작자 후원", don_title: "제작자에게 커피 한 잔 사주기", drag: "⁞⁞ 컨트롤", err: "오류", favs: "즐겨찾기", fr_hold: "길게 눌러 현재 프레임 보기", fr_none: "프레임 없음: 모델이 오프라인입니다", fs: "전체", grp_all: "모든 그룹", grp_new: "+ 새 그룹…", grp_none: "그룹 없음", hd_all: "모두 숨기기", hd_cats: "카테고리", hd_clear: "내 규칙 모두 삭제", hd_details: "세부 정보", hd_g_chat: "채팅", hd_g_site: "사이트", hd_g_win: "팝업", hd_hidden: "{m}개 중 {n}개 숨김", hd_na: "아직 찾지 못함", hd_none: "아직 규칙이 없습니다.", hd_off: "모두 표시", hd_p_clean: "깔끔한 플레이어", hd_p_min: "최소", hd_peek: "항목을 길게 누르면 무엇을 숨기는지 볼 수 있습니다", hd_pick: "요소 선택", hd_reset: "이 사이트 초기화", hd_rules: "내 규칙", hd_title: "숨기기", hide: "숨기기 (H)", hints: "M=소리 F=전체 T=극장 H=숨김", hist_clear: "비우기", hist_clear_q: "시청 기록을 모두 삭제할까요?", hist_del: "기록에서 제거", hist_empty: "아직 비어 있습니다. 영상이 재생되는 동안 시간이 집계됩니다.", hist_s_last: "최근", hist_s_time: "시간", hist_s_vis: "방문", hist_title: "기록", hist_total: "모델: {n} · 합계: {t}", hist_visits: "회", home: "홈", lang_auto: "자동", lang_title: "언어", min: "최소화", mv_abr_row: "자동 화질", mv_abr_t: "자동 화질: 영상이 끊기거나 네트워크가 약하면 화질을 낮추고, 연결이 안정되면 높입니다", mv_add: "추가", mv_auto: "자동", mv_auto_all: "모든 라이브 스트림 자동 추가", mv_back: "뒤로", mv_bg: "백그라운드 재생", mv_bg_t: "탭을 전환하거나 이 창을 닫거나 브라우저를 나가도 스트림이 계속 재생됩니다", mv_bm_h: "라이브 즐겨찾기", mv_clear_q: "모든 스트림을 닫을까요?", mv_close: "닫기", mv_cols: "레이아웃", mv_cur: "현재 스트림", mv_dvr: "되감기 버퍼", mv_dvr_off: "끔", mv_fs: "전체 화면", mv_full: "창이 가득 찼습니다", mv_go: "추가", mv_hint: "스트림을 추가해 여러 개를 동시에 시청하세요. 타일을 탭하면 소리가 그쪽으로 옮겨집니다.", mv_link_h: "또는 링크로", mv_load: "불러오는 중…", mv_min_u: "분", mv_mute: "음소거", mv_none: "라이브 즐겨찾기 없음", mv_off: "스트림을 사용할 수 없음", mv_pip: "화면 속 화면", mv_play: "재생 / 일시정지", mv_qual: "화질", mv_retry: "탭하여 다시 시도", mv_search: "라이브 즐겨찾기 검색", mv_title: "플레이어", no_vid: "영상 없음", note_ph: "메모... (#태그)", pip: "PIP", pk_cancel: "취소", pk_found: "개 일치", pk_hint: "페이지의 요소를 탭하세요", pk_ok: "숨기기", pk_up: "상위", qual: "화질", rad_media: "미디어", rad_nav: "즐겨찾기", rad_next: "다음", rad_page: "페이지", rad_prev: "이전", rad_reset: "버튼 위치 초기화", rad_title: "원형 메뉴", rad_vdn: "소리 작게", rad_view: "화면", rad_vup: "소리 크게", rec: "녹화", rec_tip: "탭: 녹화 · 길게 누르기: 설정", rel: "새로고침", rs_cur: "현재 스트림:", rs_gbh: "GB/시간", rs_hint: "파트가 선택한 크기에 도달하면 별도 파일로 저장되고 녹화는 새 파트에서 이어집니다. 데이터는 작은 단위로 디스크에 기록되며, 디스크를 사용할 수 없으면 휴대폰 메모리에 남으므로 이때는 더 작은 파트를 고르세요.", rs_mbmin: "MB/분", rs_next: "변경 사항은 다음 파트 또는 새 녹화부터 적용됩니다.", rs_now: "현재", rs_off: "끔", rs_p: "파트", rs_part: "파트 크기(파일 분할)", rs_q_hi: "높음", rs_q_low: "낮음", rs_q_mid: "보통", rs_q_min: "절약", rs_quality: "화질(비트레이트)", rs_recover: "저장되지 않은 녹화 복구", rs_resnote: "캡처 모드는 스트림 해상도로 녹화합니다. 위 옵션은 비트레이트(압축)만 바꾸며, 1080p–360p는 예상 화질을 나타냅니다. HLS 모드에서는 지정한 높이를 넘지 않는 스트림이 선택됩니다.", rs_src: "소스", rs_src_cap: "캡처(재인코딩)", rs_src_hls: "HLS(재인코딩 없음)", rs_src_note: "HLS는 스트림 세그먼트를 재인코딩 없이 그대로 다운로드하여 MP4로 저장합니다. 스트림 주소가 필요하며, 없으면 캡처가 사용됩니다. 화질은 원본 스트림과 동일한 원본 화질입니다. 녹화는 작은 체크포인트 단위로 디스크에 기록됩니다.", rs_t: "전체", rs_title: "녹화 설정", rs_total: "총 크기에서 중지", rs_upto: "최대", search: "검색", seen_now: "방금", seen_tip: "마지막 온라인", settings: "설정", sleep_custom: "직접 설정", sleep_hint: "시간이 되면 플레이어가 꺼지고 녹화 중이던 영상은 중지되어 저장됩니다. 검은 화면을 탭하면 플레이어가 돌아옵니다.", sleep_left: "남음", sleep_sec: "초", sleep_start: "시작", sleep_title: "취침 타이머", sleep_wake: "취침 타이머가 종료되었습니다.\n탭하여 계속", snap: "캡처", sort_date: "날짜", sort_last: "최근", sort_live: "라이브", sort_name: "이름", sort_viewers: "시청자", sound: "소리", st_av_refresh: "아바타 새로고침", st_away: "BRB", st_back: "뒤로", st_btns: "패널 버튼", st_btns_hint: "버튼을 숨기거나 다시 표시하려면 탭하세요.", st_btns_reset: "모든 버튼 표시", st_color: "패널 색상", st_color_rnd: "무작위", st_cols: "열 수", st_custom: "사용자 지정 색상", st_data: "데이터", st_fav: "즐겨찾기", st_favnav: "컨트롤의 “이전 / 다음” 버튼", st_group: "그룹", st_hide_grp: "그룹 선택 숨기기", st_hide_note: "메모 입력란(태그) 숨기기", st_hide_topic: "모델 소개 숨기기", st_hist: "시청 기록", st_hist_on: "기록 저장", st_hue: "색조", st_lang: "언어", st_lig: "명도", st_list: "목록", st_look: "외관", st_open: "열기", st_prev: "모델 미리보기", st_prev_off: "갱신 안 함", st_prev_spd: "미리보기 갱신", st_private: "프라이빗", st_ptags: "프로필 태그 숨기기 (BongaCams)", st_rec: "영상 녹화", st_sat: "채도", st_scale: "패널 크기", st_site_dark: "사이트 다크 테마", st_star: "썸네일 별표", st_vip: "VIP", star_add: "즐겨찾기에 추가", star_del: "즐겨찾기에서 제거", star_del_q: "이 모델에 메모나 그룹이 있습니다. 즐겨찾기에서 제거할까요?", thtr: "극장", tp_less: "[접기]", tp_more: "[더보기]", u_d: "일", u_gb: "GB", u_h: "시간", u_m: "분", u_mb: "MB", viewers_tip: "방 시청자 수", w_new: "미시청", w_seen: "시청함", }, HE: { ago: "לפני {n}", bg: "רקע", bk_csv: "טבלאות CSV", bk_csv_fav: "מועדפים", bk_csv_hist: "היסטוריה", bk_csv_ses: "סשנים", bk_exp: "ייצוא גיבוי", bk_imp: "ייבוא גיבוי", bk_p_fav: "מועדפים", bk_p_hist: "היסטוריה", bk_p_set: "הגדרות", bk_parts: "תוכן הגיבוי", bm_add: "הוסף למועדפים", bm_all: "הכול", bm_del: "מחק", bm_dup: "כבר נוסף!", bm_empty: "הרשימה ריקה.", bm_find: "חיפוש לפי שם…", bm_nomatch: "אין מודלים התואמים לסינון", bm_online: "אונליין", bm_online_t: "הצג רק אונליין", bm_pin: "הצמד למעלה", bm_priv: "פרטי", bm_title: "מועדפים", bm_unpin: "בטל הצמדה", bt_hints: "טיפים", bt_stats: "צופים", bt_vol: "עוצמת קול", cal_all_tag: "כל התגיות", cal_allday: "כל היום", cal_c_dur: "משך", cal_c_model: "מודל", cal_c_n: "פעמים", cal_c_time: "כניסה", cal_hist: "היסטוריה", cal_loaded: "לוח הזמנים נטען: {a} מתוך {b}", cal_models: "מודלים", cal_no_sched: "לוח הזמנים נלקח מפרופילי המועדפים. פתחו את המועדפים והמתינו כמה שניות.", cal_none: "אין דבר ביום זה", cal_reset: "איפוס", cal_sched: "לוח זמנים", cal_title: "לוח שנה", cal_today: "היום", cal_total: "סה״כ", cal_watched: "נצפה", cf_cancel: "ביטול", cf_ok: "כן", ch_avg: "ממוצע יומי", ch_btn: "תרשימים", ch_days: "זמן ליום, 14 ימים", ch_hours: "פעילות לפי שעה", ch_peak: "שיא", ch_top: "מובילים לפי זמן", copy: "קישור", dark: "ערכת נושא כהה / בהירה", don_copied: "הכתובת הועתקה", don_fail: "ההעתקה נכשלה", don_sub: "תודה על השימוש", don_tap: "הקש על כתובת כדי לראות אותה במלואה. הכפתור שלצידה מעתיק אותה.", don_tip: "תמיכה ביוצר", don_title: "קנה ליוצר כוס קפה", drag: "⁞⁞ בקרה", err: "שגיאה", favs: "מועדפים", fr_hold: "לחיצה ארוכה להצגת הפריים הנוכחי", fr_none: "אין פריים: המודל לא מחובר", fs: "מסך מלא", grp_all: "כל הקבוצות", grp_new: "+ קבוצה חדשה…", grp_none: "ללא קבוצה", hd_all: "הסתר הכול", hd_cats: "קטגוריות", hd_clear: "מחק את כל הכללים שלי", hd_details: "פרטים", hd_g_chat: "צ'אט", hd_g_site: "אתר", hd_g_win: "חלונות קופצים", hd_hidden: "הוסתרו {n} מתוך {m}", hd_na: "עדיין לא נמצא", hd_none: "אין כללים עדיין.", hd_off: "הצג הכול", hd_p_clean: "נגן נקי", hd_p_min: "מינימלי", hd_peek: "החזק פריט לחוץ כדי לראות מה הוא מסתיר", hd_pick: "בחר אלמנט", hd_reset: "איפוס עבור אתר זה", hd_rules: "הכללים שלי", hd_title: "הסתרה", hide: "הסתר (H)", hints: "M=קול F=מסך מלא T=תיאטרון H=הסתר", hist_clear: "נקה", hist_clear_q: "למחוק את כל היסטוריית הצפייה?", hist_del: "הסר מההיסטוריה", hist_empty: "ריק לבינתיים. הזמן נספר בזמן שהווידאו מנגן.", hist_s_last: "אחרונים", hist_s_time: "זמן", hist_s_vis: "ביקורים", hist_title: "היסטוריה", hist_total: "מודלים: {n} · סה״כ: {t}", hist_visits: "ביקורים", home: "בית", lang_auto: "אוטומטי", lang_title: "שפה", min: "מזעור", mv_abr_row: "איכות אוטומטית", mv_abr_t: "איכות אוטומטית: מורידה את האיכות כשהווידאו נתקע או כשהרשת חלשה, ומעלה אותה כשהחיבור יציב", mv_add: "הוסף", mv_auto: "אוטומטי", mv_auto_all: "הוסף אוטומטית את כל הסטרימים החיים", mv_back: "חזרה", mv_bg: "ניגון ברקע", mv_bg_t: "הסטרימים ממשיכים להתנגן כשעוברים בין לשוניות, סוגרים את החלון הזה או יוצאים מהדפדפן", mv_bm_h: "מועדפים בשידור חי", mv_clear_q: "לסגור את כל הסטרימים?", mv_close: "סגור", mv_cols: "פריסה", mv_cur: "הסטרים הנוכחי", mv_dvr: "מאגר הרצה לאחור", mv_dvr_off: "כבוי", mv_fs: "מסך מלא", mv_full: "החלון מלא", mv_go: "הוסף", mv_hint: "הוסף סטרימים כדי לצפות בכמה בו־זמנית. הקש על אריח כדי להעביר אליו את הקול.", mv_link_h: "או באמצעות קישור", mv_load: "טוען…", mv_min_u: "דק׳", mv_mute: "השתק", mv_none: "אין מועדפים בשידור חי", mv_off: "הסטרים אינו זמין", mv_pip: "תמונה בתוך תמונה", mv_play: "הפעל / השהה", mv_qual: "איכות", mv_retry: "הקש לניסיון חוזר", mv_search: "חיפוש במועדפים בשידור חי", mv_title: "נגן", no_vid: "אין וידאו", note_ph: "הערה... (#תגיות)", pip: "PIP", pk_cancel: "ביטול", pk_found: "התאמות", pk_hint: "הקש על אלמנט בדף", pk_ok: "הסתר", pk_up: "למעלה", qual: "איכות", rad_media: "מדיה", rad_nav: "מועדפים", rad_next: "הבא", rad_page: "עמוד", rad_prev: "הקודם", rad_reset: "איפוס מיקום הכפתור", rad_title: "תפריט מעגלי", rad_vdn: "שקט יותר", rad_view: "תצוגה", rad_vup: "חזק יותר", rec: "הקלטה", rec_tip: "הקשה: הקלטה · לחיצה ארוכה: הגדרות", rel: "רענון", rs_cur: "הסטרים הנוכחי:", rs_gbh: "GB/שעה", rs_hint: "כשחלק מגיע לגודל שנבחר, הוא נשמר כקובץ נפרד וההקלטה ממשיכה בחדש. הנתונים נכתבים לדיסק במקטעים קטנים; אם הדיסק אינו זמין, הם נשארים בזיכרון הטלפון, ולכן כדאי לבחור חלקים קטנים יותר.", rs_mbmin: "MB/דקה", rs_next: "השינויים חלים מהחלק הבא או מהקלטה חדשה.", rs_now: "עכשיו", rs_off: "כבוי", rs_p: "חלק", rs_part: "גודל חלק (הקובץ מתפצל)", rs_q_hi: "גבוהה", rs_q_low: "נמוכה", rs_q_mid: "בינונית", rs_q_min: "חיסכון", rs_quality: "איכות (ביטרייט)", rs_recover: "שחזור הקלטות שלא נשמרו", rs_resnote: "מצב לכידה מקליט ברזולוציית הסטרים: האפשרויות למעלה משנות רק את קצב הסיביות (דחיסה), ו-1080p–360p מציינות את איכות התמונה הצפויה. במצב HLS נבחר סטרים שגובהו אינו עולה על הגובה שנבחר.", rs_src: "מקור", rs_src_cap: "לכידה (קידוד מחדש)", rs_src_hls: "HLS (בלי קידוד מחדש)", rs_src_note: "HLS מוריד את קטעי הסטרים כפי שהם, בלי קידוד מחדש, ושומר MP4. נדרשת כתובת הסטרים; אם היא אינה זמינה, משתמשים בלכידה. האיכות מקורית, כמו בסטרים המקורי. ההקלטה נכתבת לדיסק בנקודות ביניים קטנות.", rs_t: "סה״כ", rs_title: "הגדרות הקלטה", rs_total: "עצור בגודל כולל", rs_upto: "עד", search: "חיפוש", seen_now: "הרגע", seen_tip: "נראה אונליין לאחרונה", settings: "הגדרות", sleep_custom: "זמן מותאם", sleep_hint: "בתום הזמן הנגן יכבה, וכל הקלטה תיעצר ותישמר. הקש על המסך השחור כדי להחזיר את הנגן.", sleep_left: "נותר", sleep_sec: "שנ׳", sleep_start: "התחל", sleep_title: "טיימר שינה", sleep_wake: "טיימר השינה הסתיים.\nהקש להמשך", snap: "צילום", sort_date: "תאריך", sort_last: "נראה", sort_live: "לייב", sort_name: "שם", sort_viewers: "צופים", sound: "קול", st_av_refresh: "רענון תמונות פרופיל", st_away: "BRB", st_back: "חזרה", st_btns: "כפתורי הפאנל", st_btns_hint: "הקש כדי להסתיר כפתור או להחזיר אותו.", st_btns_reset: "הצג את כל הכפתורים", st_color: "צבע הפאנל", st_color_rnd: "אקראי", st_cols: "עמודות", st_custom: "צבע מותאם", st_data: "נתונים", st_fav: "מועדפים", st_favnav: "כפתורי «הקודם / הבא» בבקרה", st_group: "קבוצה", st_hide_grp: "הסתר את בחירת הקבוצה", st_hide_note: "הסתר שדה הערה (תגיות)", st_hide_topic: "הסתר את תיאור המודל", st_hist: "היסטוריית צפייה", st_hist_on: "שמור היסטוריה", st_hue: "גוון", st_lang: "שפה", st_lig: "בהירות", st_list: "רשימה", st_look: "מראה", st_open: "פתח", st_prev: "תצוגות מקדימות של מודלים", st_prev_off: "ללא רענון", st_prev_spd: "רענון התצוגה המקדימה", st_private: "פרטי", st_ptags: "הסתר תגיות פרופיל (BongaCams)", st_rec: "הקלטת וידאו", st_sat: "רוויה", st_scale: "גודל הפאנל", st_site_dark: "ערכת נושא כהה לאתר", st_star: "כוכב על התמונות הממוזערות", st_vip: "VIP", star_add: "הוסף למועדפים", star_del: "הסר מהמועדפים", star_del_q: "למודל הזה יש הערה או קבוצה. להסיר מהמועדפים?", thtr: "תיאטרון", tp_less: "[פחות]", tp_more: "[עוד]", u_d: "י׳", u_gb: "GB", u_h: "שע׳", u_m: "דק׳", u_mb: "MB", viewers_tip: "צופים בחדר", w_new: "לא נצפה", w_seen: "נצפה", }, AR: { ago: "منذ {n}", bg: "الخلفية", bk_csv: "جداول CSV", bk_csv_fav: "المفضلة", bk_csv_hist: "السجل", bk_csv_ses: "الجلسات", bk_exp: "تصدير النسخة الاحتياطية", bk_imp: "استيراد النسخة الاحتياطية", bk_p_fav: "المفضلة", bk_p_hist: "السجل", bk_p_set: "الإعدادات", bk_parts: "محتوى النسخة الاحتياطية", bm_add: "إضافة إلى المفضلة", bm_all: "الكل", bm_del: "حذف", bm_dup: "تمت الإضافة مسبقًا!", bm_empty: "القائمة فارغة.", bm_find: "البحث بالاسم…", bm_nomatch: "لا يوجد موديل يطابق الفلتر", bm_online: "متصل", bm_online_t: "إظهار المتصلين فقط", bm_pin: "تثبيت في الأعلى", bm_priv: "خاص", bm_title: "المفضلة", bm_unpin: "إلغاء التثبيت", bt_hints: "تلميحات", bt_stats: "المشاهدون", bt_vol: "الصوت", cal_all_tag: "كل الوسوم", cal_allday: "طوال اليوم", cal_c_dur: "المدة", cal_c_model: "العارضة", cal_c_n: "المرات", cal_c_time: "الدخول", cal_hist: "السجل", cal_loaded: "تم تحميل الجدول: {a} من {b}", cal_models: "العارضات", cal_no_sched: "يؤخذ الجدول من ملفات المفضلة. افتح المفضلة وانتظر بضع ثوانٍ.", cal_none: "لا شيء في هذا اليوم", cal_reset: "إعادة ضبط", cal_sched: "الجدول", cal_title: "التقويم", cal_today: "اليوم", cal_total: "المجموع", cal_watched: "تمت المشاهدة", cf_cancel: "إلغاء", cf_ok: "نعم", ch_avg: "المتوسط اليومي", ch_btn: "الرسوم البيانية", ch_days: "الوقت يوميًا، 14 يومًا", ch_hours: "النشاط حسب الساعة", ch_peak: "الذروة", ch_top: "الأعلى حسب الوقت", copy: "رابط", dark: "مظهر داكن / فاتح", don_copied: "تم نسخ العنوان", don_fail: "فشل النسخ", don_sub: "شكرًا لاستخدامك الأداة", don_tap: "اضغط على العنوان لعرضه كاملًا. الزر المجاور له ينسخه.", don_tip: "ادعم المطوّر", don_title: "اشترِ للمطوّر فنجان قهوة", drag: "⁞⁞ التحكم", err: "خطأ", favs: "المفضلة", fr_hold: "اضغط مطولًا لعرض اللقطة الحالية", fr_none: "لا توجد لقطة: الموديل غير متصل", fs: "ملء الشاشة", grp_all: "كل المجموعات", grp_new: "+ مجموعة جديدة…", grp_none: "بدون مجموعة", hd_all: "إخفاء الكل", hd_cats: "الفئات", hd_clear: "حذف جميع قواعدي", hd_details: "التفاصيل", hd_g_chat: "الدردشة", hd_g_site: "الموقع", hd_g_win: "النوافذ المنبثقة", hd_hidden: "تم إخفاء {n} من {m}", hd_na: "لم يُعثر عليه بعد", hd_none: "لا توجد قواعد بعد.", hd_off: "إظهار الكل", hd_p_clean: "مشغّل نظيف", hd_p_min: "الحد الأدنى", hd_peek: "اضغط مطولًا على عنصر لترى ما الذي يخفيه", hd_pick: "اختيار عنصر", hd_reset: "إعادة الضبط لهذا الموقع", hd_rules: "قواعدي", hd_title: "الإخفاء", hide: "إخفاء (H)", hints: "M=الصوت F=ملء الشاشة T=المسرح H=إخفاء", hist_clear: "مسح", hist_clear_q: "حذف كل سجل المشاهدة؟", hist_del: "إزالة من السجل", hist_empty: "فارغ حتى الآن. يُحتسب الوقت أثناء تشغيل الفيديو.", hist_s_last: "الأحدث", hist_s_time: "الوقت", hist_s_vis: "الزيارات", hist_title: "السجل", hist_total: "الموديلات: {n} · الإجمالي: {t}", hist_visits: "زيارات", home: "الرئيسية", lang_auto: "تلقائي", lang_title: "اللغة", min: "تصغير", mv_abr_row: "جودة تلقائية", mv_abr_t: "جودة تلقائية: تخفّض الجودة عند التقطيع أو ضعف الشبكة وترفعها عندما يكون الاتصال مستقرًا", mv_add: "إضافة", mv_auto: "تلقائي", mv_auto_all: "إضافة كل البثوث المباشرة تلقائيًا", mv_back: "رجوع", mv_bg: "التشغيل في الخلفية", mv_bg_t: "تستمر البثوث في التشغيل عند تبديل علامة التبويب أو إغلاق هذه النافذة أو مغادرة المتصفح", mv_bm_h: "المفضلة المباشرة", mv_clear_q: "إغلاق كل البثوث؟", mv_close: "إغلاق", mv_cols: "التخطيط", mv_cur: "البث الحالي", mv_dvr: "مخزن الإرجاع", mv_dvr_off: "إيقاف", mv_fs: "ملء الشاشة", mv_full: "النافذة ممتلئة", mv_go: "إضافة", mv_hint: "أضف بثوثًا لمشاهدة عدة بثوث معًا. اضغط على بلاطة لنقل الصوت إليها.", mv_link_h: "أو عبر رابط", mv_load: "جارٍ التحميل…", mv_min_u: "د", mv_mute: "كتم الصوت", mv_none: "لا توجد مفضلة مباشرة", mv_off: "البث غير متاح", mv_pip: "صورة داخل صورة", mv_play: "تشغيل / إيقاف مؤقت", mv_qual: "الجودة", mv_retry: "اضغط لإعادة المحاولة", mv_search: "البحث في المفضلة المباشرة", mv_title: "المشغّل", no_vid: "لا فيديو", note_ph: "ملاحظة... (#وسوم)", pip: "PIP", pk_cancel: "إلغاء", pk_found: "تطابق", pk_hint: "اضغط على عنصر في الصفحة", pk_ok: "إخفاء", pk_up: "أعلى", qual: "الجودة", rad_media: "الوسائط", rad_nav: "المفضلة", rad_next: "التالي", rad_page: "الصفحة", rad_prev: "السابق", rad_reset: "إعادة ضبط موضع الزر", rad_title: "القائمة الدائرية", rad_vdn: "أخفض", rad_view: "العرض", rad_vup: "أعلى", rec: "تسجيل", rec_tip: "نقرة: تسجيل · ضغط مطوّل: الإعدادات", rel: "تحديث", rs_cur: "البث الحالي:", rs_gbh: "GB/س", rs_hint: "عندما يصل الجزء إلى الحجم المختار يُحفظ كملف مستقل ويتابع التسجيل في جزء جديد. تُكتب البيانات على القرص بكتل صغيرة؛ وإذا لم يكن القرص متاحًا تبقى في ذاكرة الهاتف، ولذلك يُفضَّل اختيار أجزاء أصغر.", rs_mbmin: "MB/د", rs_next: "تسري التغييرات من الجزء التالي أو من تسجيل جديد.", rs_now: "الآن", rs_off: "إيقاف", rs_p: "جزء", rs_part: "حجم الجزء (يُقسَّم الملف)", rs_q_hi: "عالية", rs_q_low: "منخفضة", rs_q_mid: "متوسطة", rs_q_min: "توفير", rs_quality: "الجودة (معدل البت)", rs_recover: "استعادة التسجيلات غير المحفوظة", rs_resnote: "يسجّل وضع الالتقاط بدقة البث: الخيارات أعلاه تغيّر معدل البت (الضغط) فقط، وتشير 1080p–360p إلى جودة الصورة المتوقعة. في وضع HLS يُختار بث لا يزيد ارتفاعه على الارتفاع المحدد.", rs_src: "المصدر", rs_src_cap: "الالتقاط (إعادة ترميز)", rs_src_hls: "HLS (بدون إعادة ترميز)", rs_src_note: "يقوم HLS بتنزيل مقاطع البث كما هي دون إعادة ترميز ويحفظ MP4. يحتاج إلى عنوان البث؛ وإن لم يتوفر يُستخدم الالتقاط. الجودة أصلية كما في البث المصدر. يُكتب التسجيل على القرص على نقاط حفظ صغيرة.", rs_t: "الإجمالي", rs_title: "إعدادات التسجيل", rs_total: "إيقاف عند الحجم الإجمالي", rs_upto: "حتى", search: "بحث", seen_now: "الآن", seen_tip: "آخر ظهور على الإنترنت", settings: "الإعدادات", sleep_custom: "وقت مخصص", sleep_hint: "عند انتهاء الوقت يُطفأ المشغّل ويتوقف أي تسجيل ويُحفظ. المس الشاشة السوداء لإعادته.", sleep_left: "المتبقي", sleep_sec: "ث", sleep_start: "ابدأ", sleep_title: "مؤقّت النوم", sleep_wake: "انتهى مؤقّت النوم.\nالمس للمتابعة", snap: "لقطة", sort_date: "التاريخ", sort_last: "آخر ظهور", sort_live: "مباشر", sort_name: "الاسم", sort_viewers: "المشاهدون", sound: "الصوت", st_av_refresh: "تحديث الصور الرمزية", st_away: "BRB", st_back: "رجوع", st_btns: "أزرار اللوحة", st_btns_hint: "المس لإخفاء زر أو إعادته.", st_btns_reset: "إظهار كل الأزرار", st_color: "لون اللوحة", st_color_rnd: "عشوائي", st_cols: "الأعمدة", st_custom: "لون مخصص", st_data: "البيانات", st_fav: "المفضلة", st_favnav: "أزرار «السابق / التالي» في لوحة التحكم", st_group: "مجموعة", st_hide_grp: "إخفاء اختيار المجموعة", st_hide_note: "إخفاء حقل الملاحظة (الوسوم)", st_hide_topic: "إخفاء وصف الموديل", st_hist: "سجل المشاهدة", st_hist_on: "حفظ السجل", st_hue: "التدرج اللوني", st_lang: "اللغة", st_lig: "السطوع", st_list: "القائمة", st_look: "المظهر", st_open: "فتح", st_prev: "معاينات الموديلات", st_prev_off: "بدون تحديث", st_prev_spd: "تحديث المعاينة", st_private: "خاص", st_ptags: "إخفاء وسوم الملف الشخصي (BongaCams)", st_rec: "تسجيل الفيديو", st_sat: "التشبع", st_scale: "حجم اللوحة", st_site_dark: "الوضع الداكن للموقع", st_star: "نجمة على الصور المصغرة", st_vip: "VIP", star_add: "إضافة إلى المفضلة", star_del: "إزالة من المفضلة", star_del_q: "لدى هذا الموديل ملاحظة أو مجموعة. هل تريد إزالته من المفضلة؟", thtr: "المسرح", tp_less: "[أقل]", tp_more: "[المزيد]", u_d: "ي", u_gb: "GB", u_h: "س", u_m: "د", u_mb: "MB", viewers_tip: "المشاهدون في الغرفة", w_new: "لم تتم المشاهدة", w_seen: "تمت المشاهدة", }, HI: { ago: "{n} पहले", bg: "बैकग्राउंड", bk_csv: "CSV तालिकाएँ", bk_csv_fav: "पसंदीदा", bk_csv_hist: "इतिहास", bk_csv_ses: "सेशन", bk_exp: "बैकअप निर्यात करें", bk_imp: "बैकअप आयात करें", bk_p_fav: "पसंदीदा", bk_p_hist: "इतिहास", bk_p_set: "सेटिंग्स", bk_parts: "बैकअप की सामग्री", bm_add: "पसंदीदा में जोड़ें", bm_all: "सभी", bm_del: "हटाएँ", bm_dup: "पहले ही जोड़ा जा चुका है!", bm_empty: "सूची खाली है।", bm_find: "नाम से खोजें…", bm_nomatch: "फ़िल्टर से कोई मॉडल मेल नहीं खाता", bm_online: "ऑनलाइन", bm_online_t: "सिर्फ़ ऑनलाइन दिखाएँ", bm_pin: "ऊपर पिन करें", bm_priv: "प्राइवेट", bm_title: "पसंदीदा", bm_unpin: "पिन हटाएँ", bt_hints: "सुझाव", bt_stats: "दर्शक", bt_vol: "आवाज़", cal_all_tag: "सभी टैग", cal_allday: "पूरे दिन", cal_c_dur: "अवधि", cal_c_model: "मॉडल", cal_c_n: "बार", cal_c_time: "प्रवेश", cal_hist: "इतिहास", cal_loaded: "शेड्यूल लोड हुआ: {a}/{b}", cal_models: "मॉडल", cal_no_sched: "शेड्यूल पसंदीदा की प्रोफ़ाइल से लिया जाता है। पसंदीदा खोलें और कुछ सेकंड प्रतीक्षा करें।", cal_none: "इस दिन कुछ नहीं है", cal_reset: "रीसेट", cal_sched: "शेड्यूल", cal_title: "कैलेंडर", cal_today: "आज", cal_total: "कुल", cal_watched: "देखा", cf_cancel: "रद्द करें", cf_ok: "हाँ", ch_avg: "दैनिक औसत", ch_btn: "चार्ट", ch_days: "प्रति दिन समय, 14 दिन", ch_hours: "घंटे के अनुसार गतिविधि", ch_peak: "शिखर", ch_top: "समय के अनुसार शीर्ष", copy: "लिंक", dark: "डार्क / लाइट थीम", don_copied: "पता कॉपी हो गया", don_fail: "कॉपी नहीं हो सका", don_sub: "इस्तेमाल करने के लिए धन्यवाद", don_tap: "पूरा पता देखने के लिए उस पर टैप करें। दाईं ओर का बटन उसे कॉपी करता है।", don_tip: "लेखक का समर्थन करें", don_title: "लेखक को एक कॉफ़ी पिलाएँ", drag: "⁞⁞ नियंत्रण", err: "त्रुटि", favs: "पसंदीदा", fr_hold: "मौजूदा फ़्रेम देखने के लिए दबाए रखें", fr_none: "फ़्रेम उपलब्ध नहीं: मॉडल ऑफ़लाइन है", fs: "फुलस्क्रीन", grp_all: "सभी ग्रुप", grp_new: "+ नया ग्रुप…", grp_none: "कोई ग्रुप नहीं", hd_all: "सब छिपाएँ", hd_cats: "श्रेणियाँ", hd_clear: "मेरे सभी नियम हटाएँ", hd_details: "विवरण", hd_g_chat: "चैट", hd_g_site: "साइट", hd_g_win: "पॉप-अप", hd_hidden: "{m} में से {n} छिपे", hd_na: "अभी नहीं मिला", hd_none: "अभी कोई नियम नहीं।", hd_off: "सब दिखाएँ", hd_p_clean: "साफ़ प्लेयर", hd_p_min: "न्यूनतम", hd_peek: "कोई आइटम दबाए रखें, देखने के लिए कि वह क्या छिपाता है", hd_pick: "एलिमेंट चुनें", hd_reset: "इस साइट के लिए रीसेट करें", hd_rules: "मेरे नियम", hd_title: "छिपाना", hide: "छिपाएँ (H)", hints: "M=आवाज़ F=फुलस्क्रीन T=थिएटर H=छिपाएँ", hist_clear: "साफ़ करें", hist_clear_q: "पूरा देखने का इतिहास हटाएँ?", hist_del: "इतिहास से हटाएँ", hist_empty: "अभी खाली है। वीडियो चलने पर ही समय गिना जाता है।", hist_s_last: "हाल के", hist_s_time: "समय", hist_s_vis: "विज़िट", hist_title: "इतिहास", hist_total: "मॉडल: {n} · कुल: {t}", hist_visits: "विज़िट", home: "होम", lang_auto: "ऑटो", lang_title: "भाषा", min: "छोटा करें", mv_abr_row: "ऑटो क्वालिटी", mv_abr_t: "ऑटो क्वालिटी: वीडियो अटकने या नेटवर्क कमज़ोर होने पर क्वालिटी घटाती है और कनेक्शन स्थिर होने पर बढ़ाती है", mv_add: "जोड़ें", mv_auto: "ऑटो", mv_auto_all: "सभी लाइव स्ट्रीम अपने-आप जोड़ें", mv_back: "वापस", mv_bg: "बैकग्राउंड में चलाएँ", mv_bg_t: "टैब बदलने, यह विंडो बंद करने या ब्राउज़र छोड़ने पर भी स्ट्रीम चलती रहती हैं", mv_bm_h: "लाइव बुकमार्क", mv_clear_q: "सभी स्ट्रीम बंद करें?", mv_close: "बंद करें", mv_cols: "लेआउट", mv_cur: "मौजूदा स्ट्रीम", mv_dvr: "रिवाइंड बफ़र", mv_dvr_off: "बंद", mv_fs: "फुलस्क्रीन", mv_full: "विंडो भरी हुई है", mv_go: "जोड़ें", mv_hint: "एक साथ कई स्ट्रीम देखने के लिए स्ट्रीम जोड़ें। आवाज़ किसी टाइल पर ले जाने के लिए उस पर टैप करें।", mv_link_h: "या लिंक से", mv_load: "लोड हो रहा है…", mv_min_u: "मिनट", mv_mute: "म्यूट", mv_none: "कोई लाइव बुकमार्क नहीं", mv_off: "स्ट्रीम उपलब्ध नहीं", mv_pip: "पिक्चर-इन-पिक्चर", mv_play: "चलाएँ / रोकें", mv_qual: "क्वालिटी", mv_retry: "दोबारा कोशिश के लिए टैप करें", mv_search: "लाइव बुकमार्क खोजें", mv_title: "प्लेयर", no_vid: "वीडियो नहीं", note_ph: "नोट... (#टैग)", pip: "PIP", pk_cancel: "रद्द करें", pk_found: "मिलान", pk_hint: "पेज पर किसी एलिमेंट को टैप करें", pk_ok: "छिपाएँ", pk_up: "ऊपर", qual: "क्वालिटी", rad_media: "मीडिया", rad_nav: "पसंदीदा", rad_next: "अगला", rad_page: "पेज", rad_prev: "पिछला", rad_reset: "बटन की स्थिति रीसेट करें", rad_title: "गोलाकार मेनू", rad_vdn: "आवाज़ घटाएँ", rad_view: "दृश्य", rad_vup: "आवाज़ बढ़ाएँ", rec: "रिकॉर्ड", rec_tip: "टैप: रिकॉर्ड · दबाए रखें: सेटिंग्स", rel: "रीलोड", rs_cur: "मौजूदा स्ट्रीम:", rs_gbh: "GB/घंटा", rs_hint: "जब कोई पार्ट चुने गए आकार तक पहुँचता है, तो वह अलग फ़ाइल के रूप में सेव हो जाता है और रिकॉर्डिंग नए पार्ट में जारी रहती है। डेटा छोटे-छोटे हिस्सों में डिस्क पर लिखा जाता है; डिस्क उपलब्ध न हो तो वह फ़ोन की मेमोरी में रहता है, तब छोटे पार्ट चुनना बेहतर है।", rs_mbmin: "MB/मिनट", rs_next: "बदलाव अगले पार्ट या नई रिकॉर्डिंग से लागू होते हैं।", rs_now: "अभी", rs_off: "बंद", rs_p: "पार्ट", rs_part: "पार्ट का आकार (फ़ाइल बँटती है)", rs_q_hi: "उच्च", rs_q_low: "कम", rs_q_mid: "मध्यम", rs_q_min: "बचत", rs_quality: "क्वालिटी (बिटरेट)", rs_recover: "सेव न हुई रिकॉर्डिंग वापस पाएँ", rs_resnote: "कैप्चर मोड स्ट्रीम के रेज़ोल्यूशन पर रिकॉर्ड करता है: ऊपर के विकल्प सिर्फ़ बिटरेट (कंप्रेशन) बदलते हैं, और 1080p–360p अपेक्षित तस्वीर की क्वालिटी बताते हैं। HLS मोड में चुनी गई ऊँचाई से ज़्यादा ऊँची स्ट्रीम नहीं चुनी जाती।", rs_src: "स्रोत", rs_src_cap: "कैप्चर (री-एन्कोड)", rs_src_hls: "HLS (बिना री-एन्कोड)", rs_src_note: "HLS स्ट्रीम के सेगमेंट जैसे हैं वैसे ही, बिना री-एन्कोड किए, डाउनलोड करता है और MP4 सेव करता है। इसके लिए स्ट्रीम का पता चाहिए; न हो तो कैप्चर इस्तेमाल होता है। क्वालिटी मूल रहती है, स्रोत स्ट्रीम जैसी। रिकॉर्डिंग छोटे चेकपॉइंट में डिस्क पर लिखी जाती है।", rs_t: "कुल", rs_title: "रिकॉर्डिंग सेटिंग्स", rs_total: "कुल आकार पर रोकें", rs_upto: "तक", search: "खोज", seen_now: "अभी-अभी", seen_tip: "आख़िरी बार ऑनलाइन", settings: "सेटिंग्स", sleep_custom: "अपना समय", sleep_hint: "समय पूरा होने पर प्लेयर बंद हो जाएगा, और चल रही रिकॉर्डिंग रुककर सेव हो जाएगी। प्लेयर वापस लाने के लिए काली स्क्रीन पर टैप करें।", sleep_left: "शेष", sleep_sec: "सेक", sleep_start: "शुरू करें", sleep_title: "स्लीप टाइमर", sleep_wake: "स्लीप टाइमर पूरा हुआ।\nजारी रखने के लिए टैप करें", snap: "स्क्रीनशॉट", sort_date: "तारीख़", sort_last: "आख़िरी बार", sort_live: "लाइव", sort_name: "नाम", sort_viewers: "दर्शक", sound: "आवाज़", st_av_refresh: "अवतार रीफ़्रेश करें", st_away: "BRB", st_back: "वापस", st_btns: "पैनल बटन", st_btns_hint: "किसी बटन को छिपाने या वापस लाने के लिए टैप करें।", st_btns_reset: "सभी बटन दिखाएँ", st_color: "पैनल का रंग", st_color_rnd: "रैंडम", st_cols: "कॉलम", st_custom: "अपना रंग", st_data: "डेटा", st_fav: "पसंदीदा", st_favnav: "कंट्रोल पर «पिछला / अगला» बटन", st_group: "ग्रुप", st_hide_grp: "ग्रुप चयन छिपाएँ", st_hide_note: "नोट फ़ील्ड (टैग) छिपाएँ", st_hide_topic: "मॉडल का विवरण छिपाएँ", st_hist: "देखने का इतिहास", st_hist_on: "इतिहास सहेजें", st_hue: "रंगत", st_lang: "भाषा", st_lig: "चमक", st_list: "सूची", st_look: "दिखावट", st_open: "खोलें", st_prev: "मॉडल प्रीव्यू", st_prev_off: "रीफ़्रेश नहीं", st_prev_spd: "प्रीव्यू रीफ़्रेश", st_private: "प्राइवेट", st_ptags: "प्रोफ़ाइल टैग छिपाएँ (BongaCams)", st_rec: "वीडियो रिकॉर्डिंग", st_sat: "संतृप्ति", st_scale: "पैनल का आकार", st_site_dark: "साइट की डार्क थीम", st_star: "थंबनेल पर तारा", st_vip: "VIP", star_add: "पसंदीदा में जोड़ें", star_del: "पसंदीदा से हटाएँ", star_del_q: "इस मॉडल का नोट या ग्रुप है। पसंदीदा से हटाएँ?", thtr: "थिएटर", tp_less: "[कम]", tp_more: "[और]", u_d: "दिन", u_gb: "GB", u_h: "घं", u_m: "मिनट", u_mb: "MB", viewers_tip: "कमरे में दर्शक", w_new: "नहीं देखा", w_seen: "देखा", }, ID: { ago: "{n} lalu", bg: "LATAR", bk_csv: "Tabel CSV", bk_csv_fav: "Favorit", bk_csv_hist: "Riwayat", bk_csv_ses: "Sesi", bk_exp: "Ekspor cadangan", bk_imp: "Impor cadangan", bk_p_fav: "Favorit", bk_p_hist: "Riwayat", bk_p_set: "Pengaturan", bk_parts: "Isi cadangan", bm_add: "Tambah ke favorit", bm_all: "Semua", bm_del: "Hapus", bm_dup: "Sudah ditambahkan!", bm_empty: "Daftar kosong.", bm_find: "Cari berdasarkan nama…", bm_nomatch: "Tidak ada model yang cocok dengan filter", bm_online: "Online", bm_online_t: "Tampilkan hanya online", bm_pin: "Sematkan di atas", bm_priv: "Priv", bm_title: "Favorit", bm_unpin: "Lepas sematan", bt_hints: "Petunjuk", bt_stats: "Penonton", bt_vol: "Volume", cal_all_tag: "Semua tag", cal_allday: "Sepanjang hari", cal_c_dur: "Durasi", cal_c_model: "Model", cal_c_n: "Kali", cal_c_time: "Masuk", cal_hist: "Riwayat", cal_loaded: "Jadwal dimuat: {a} dari {b}", cal_models: "Model", cal_no_sched: "Jadwal diambil dari profil favorit Anda. Buka Favorit dan tunggu beberapa detik.", cal_none: "Tidak ada apa pun di hari ini", cal_reset: "Atur ulang", cal_sched: "Jadwal", cal_title: "Kalender", cal_today: "Hari ini", cal_total: "Total", cal_watched: "Ditonton", cf_cancel: "Batal", cf_ok: "Ya", ch_avg: "Rata-rata harian", ch_btn: "Grafik", ch_days: "Waktu per hari, 14 hari", ch_hours: "Aktivitas per jam", ch_peak: "Puncak", ch_top: "Teratas menurut waktu", copy: "TAUTAN", dark: "Tema gelap / terang", don_copied: "Alamat disalin", don_fail: "Gagal menyalin", don_sub: "Terima kasih telah menggunakannya", don_tap: "Ketuk alamat untuk melihatnya utuh. Tombol di kanan menyalinnya.", don_tip: "Dukung pembuat", don_title: "Traktir pembuat secangkir kopi", drag: "⁞⁞ KONTROL", err: "ERROR", favs: "FAVORIT", fr_hold: "Tahan untuk melihat bingkai saat ini", fr_none: "Tidak ada bingkai: model sedang offline", fs: "PENUH", grp_all: "Semua grup", grp_new: "+ Grup baru…", grp_none: "Tanpa grup", hd_all: "Sembunyikan semua", hd_cats: "Kategori", hd_clear: "Hapus semua aturan saya", hd_details: "Detail", hd_g_chat: "Obrolan", hd_g_site: "Situs", hd_g_win: "Pop-up", hd_hidden: "{n} dari {m} disembunyikan", hd_na: "belum ditemukan", hd_none: "Belum ada aturan.", hd_off: "Tampilkan semua", hd_p_clean: "Pemutar bersih", hd_p_min: "Minimal", hd_peek: "Tahan item untuk melihat apa yang disembunyikannya", hd_pick: "Pilih elemen", hd_reset: "Setel ulang untuk situs ini", hd_rules: "Aturan saya", hd_title: "Penyembunyian", hide: "Sembunyikan (H)", hints: "M=SUARA F=PENUH T=TEATER H=SEMBUNYI", hist_clear: "Hapus", hist_clear_q: "Hapus seluruh riwayat tontonan?", hist_del: "Hapus dari riwayat", hist_empty: "Masih kosong. Waktu dihitung saat video diputar.", hist_s_last: "Terbaru", hist_s_time: "Waktu", hist_s_vis: "Kunjungan", hist_title: "Riwayat", hist_total: "Model: {n} · Total: {t}", hist_visits: "kunjungan", home: "Beranda", lang_auto: "Otomatis", lang_title: "Bahasa", min: "Perkecil", mv_abr_row: "Kualitas otomatis", mv_abr_t: "Kualitas otomatis: menurunkan kualitas saat video tersendat atau jaringan lemah, dan menaikkannya saat koneksi stabil", mv_add: "Tambah", mv_auto: "Otomatis", mv_auto_all: "Tambahkan semua stream live secara otomatis", mv_back: "Kembali", mv_bg: "Putar di latar belakang", mv_bg_t: "Stream tetap diputar saat Anda berpindah tab, menutup jendela ini, atau keluar dari browser", mv_bm_h: "Bookmark live", mv_clear_q: "Tutup semua stream?", mv_close: "Tutup", mv_cols: "Tata letak", mv_cur: "Stream saat ini", mv_dvr: "Buffer putar ulang", mv_dvr_off: "Mati", mv_fs: "Layar penuh", mv_full: "Jendela penuh", mv_go: "Tambah", mv_hint: "Tambahkan stream untuk menonton beberapa sekaligus. Ketuk sebuah kotak untuk memindahkan suara ke sana.", mv_link_h: "Atau lewat tautan", mv_load: "Memuat…", mv_min_u: "mnt", mv_mute: "Bisukan", mv_none: "Tidak ada bookmark live", mv_off: "Stream tidak tersedia", mv_pip: "Gambar dalam gambar", mv_play: "Putar / jeda", mv_qual: "Kualitas", mv_retry: "ketuk untuk mencoba lagi", mv_search: "Cari bookmark live", mv_title: "Pemutar", no_vid: "TANPA VIDEO", note_ph: "Catatan... (#tag)", pip: "PIP", pk_cancel: "Batal", pk_found: "cocok", pk_hint: "Ketuk elemen di halaman", pk_ok: "Sembunyikan", pk_up: "Naik", qual: "KUALITAS", rad_media: "Media", rad_nav: "Favorit", rad_next: "Berikutnya", rad_page: "Halaman", rad_prev: "Sebelumnya", rad_reset: "Atur ulang posisi tombol", rad_title: "Menu radial", rad_vdn: "Lebih pelan", rad_view: "Tampilan", rad_vup: "Lebih keras", rec: "REKAM", rec_tip: "Ketuk: rekam · Tahan: pengaturan", rel: "Muat ulang", rs_cur: "Stream saat ini:", rs_gbh: "GB/j", rs_hint: "Saat sebuah bagian mencapai ukuran yang dipilih, bagian itu disimpan sebagai file terpisah dan rekaman berlanjut di bagian baru. Data ditulis ke disk dalam blok kecil; jika disk tidak tersedia, data tetap di memori ponsel, jadi pilih bagian yang lebih kecil.", rs_mbmin: "MB/mnt", rs_next: "Perubahan berlaku mulai bagian berikutnya atau rekaman baru.", rs_now: "Sekarang", rs_off: "Mati", rs_p: "bagian", rs_part: "Ukuran bagian (file dipecah)", rs_q_hi: "Tinggi", rs_q_low: "Rendah", rs_q_mid: "Sedang", rs_q_min: "Hemat", rs_quality: "Kualitas (bitrate)", rs_recover: "Pulihkan rekaman yang belum disimpan", rs_resnote: "Capture merekam dengan resolusi stream: opsi di atas hanya mengubah bitrate (kompresi), dan 1080p–360p menunjukkan perkiraan kualitas gambar. Pada mode HLS, dipilih stream dengan tinggi tidak melebihi yang ditetapkan.", rs_src: "Sumber", rs_src_cap: "Tangkap (encode ulang)", rs_src_hls: "HLS (tanpa encode ulang)", rs_src_note: "HLS mengunduh segmen stream apa adanya, tanpa encode ulang, dan menyimpan MP4. Memerlukan alamat stream; jika tidak tersedia, digunakan Tangkap. Kualitasnya asli, sama seperti stream sumber. Rekaman ditulis ke disk dalam titik pemeriksaan kecil.", rs_t: "total", rs_title: "Pengaturan rekaman", rs_total: "Berhenti pada ukuran total", rs_upto: "hingga", search: "CARI", seen_now: "baru saja", seen_tip: "Terakhir online", settings: "Pengaturan", sleep_custom: "Waktu kustom", sleep_hint: "Saat waktu habis, pemutar dimatikan dan rekaman yang berjalan dihentikan lalu disimpan. Ketuk layar hitam untuk mengembalikannya.", sleep_left: "Sisa", sleep_sec: "dtk", sleep_start: "Mulai", sleep_title: "Timer tidur", sleep_wake: "Timer tidur selesai.\nKetuk untuk melanjutkan", snap: "FOTO", sort_date: "Tanggal", sort_last: "Terakhir dilihat", sort_live: "Live", sort_name: "Nama", sort_viewers: "Penonton", sound: "SUARA", st_av_refresh: "Segarkan avatar", st_away: "BRB", st_back: "Kembali", st_btns: "Tombol panel", st_btns_hint: "Ketuk untuk menyembunyikan tombol atau memunculkannya kembali.", st_btns_reset: "Tampilkan semua tombol", st_color: "Warna panel", st_color_rnd: "Acak", st_cols: "Kolom", st_custom: "Warna kustom", st_data: "Data", st_fav: "Favorit", st_favnav: "Tombol «sebelumnya / berikutnya» di kontrol", st_group: "Grup", st_hide_grp: "Sembunyikan pilihan grup", st_hide_note: "Sembunyikan kolom catatan (tag)", st_hide_topic: "Sembunyikan deskripsi model", st_hist: "Riwayat tontonan", st_hist_on: "Simpan riwayat", st_hue: "Rona", st_lang: "Bahasa", st_lig: "Kecerahan", st_list: "Daftar", st_look: "Tampilan", st_open: "Buka", st_prev: "Pratinjau model", st_prev_off: "Tanpa pembaruan", st_prev_spd: "Penyegaran pratinjau", st_private: "Privat", st_ptags: "Sembunyikan tag profil (BongaCams)", st_rec: "Perekaman video", st_sat: "Saturasi", st_scale: "Ukuran panel", st_site_dark: "Tema gelap situs", st_star: "Bintang di thumbnail", st_vip: "VIP", star_add: "Tambah ke favorit", star_del: "Hapus dari favorit", star_del_q: "Model ini punya catatan atau grup. Hapus dari favorit?", thtr: "TEATER", tp_less: "[Lebih sedikit]", tp_more: "[Selengkapnya]", u_d: "h", u_gb: "GB", u_h: "j", u_m: "mnt", u_mb: "MB", viewers_tip: "Penonton di ruangan", w_new: "Belum ditonton", w_seen: "Ditonton", }, VI: { ago: "{n} trước", bg: "NỀN", bk_csv: "Bảng CSV", bk_csv_fav: "Yêu thích", bk_csv_hist: "Lịch sử", bk_csv_ses: "Phiên", bk_exp: "Xuất bản sao lưu", bk_imp: "Nhập bản sao lưu", bk_p_fav: "Yêu thích", bk_p_hist: "Lịch sử", bk_p_set: "Cài đặt", bk_parts: "Nội dung bản sao lưu", bm_add: "Thêm vào yêu thích", bm_all: "Tất cả", bm_del: "Xóa", bm_dup: "Đã thêm rồi!", bm_empty: "Danh sách trống.", bm_find: "Tìm theo tên…", bm_nomatch: "Không có model khớp bộ lọc", bm_online: "Online", bm_online_t: "Chỉ hiện online", bm_pin: "Ghim lên đầu", bm_priv: "Riêng", bm_title: "Yêu thích", bm_unpin: "Bỏ ghim", bt_hints: "Gợi ý", bt_stats: "Người xem", bt_vol: "Âm lượng", cal_all_tag: "Tất cả thẻ", cal_allday: "Cả ngày", cal_c_dur: "Thời lượng", cal_c_model: "Người mẫu", cal_c_n: "Lần", cal_c_time: "Vào", cal_hist: "Lịch sử", cal_loaded: "Đã tải lịch: {a}/{b}", cal_models: "Người mẫu", cal_no_sched: "Lịch phát lấy từ hồ sơ của mục yêu thích. Hãy mở Yêu thích và chờ vài giây.", cal_none: "Hôm nay không có gì", cal_reset: "Đặt lại", cal_sched: "Lịch phát", cal_title: "Lịch", cal_today: "Hôm nay", cal_total: "Tổng", cal_watched: "Đã xem", cf_cancel: "Hủy", cf_ok: "Có", ch_avg: "Trung bình mỗi ngày", ch_btn: "Biểu đồ", ch_days: "Thời gian mỗi ngày, 14 ngày", ch_hours: "Hoạt động theo giờ", ch_peak: "Đỉnh", ch_top: "Xếp hạng theo thời gian", copy: "LIÊN KẾT", dark: "Giao diện tối / sáng", don_copied: "Đã sao chép địa chỉ", don_fail: "Sao chép thất bại", don_sub: "Cảm ơn bạn đã sử dụng", don_tap: "Chạm vào địa chỉ để xem đầy đủ. Nút bên phải sẽ sao chép nó.", don_tip: "Ủng hộ tác giả", don_title: "Mời tác giả một ly cà phê", drag: "⁞⁞ ĐIỀU KHIỂN", err: "LỖI", favs: "YÊU THÍCH", fr_hold: "Giữ để xem khung hình hiện tại", fr_none: "Không có khung hình: model đang offline", fs: "TOÀN MH", grp_all: "Tất cả nhóm", grp_new: "+ Nhóm mới…", grp_none: "Không nhóm", hd_all: "Ẩn tất cả", hd_cats: "Danh mục", hd_clear: "Xóa tất cả quy tắc của tôi", hd_details: "Chi tiết", hd_g_chat: "Trò chuyện", hd_g_site: "Trang web", hd_g_win: "Cửa sổ bật lên", hd_hidden: "Đã ẩn {n} trên {m}", hd_na: "chưa tìm thấy", hd_none: "Chưa có quy tắc.", hd_off: "Hiện tất cả", hd_p_clean: "Trình phát sạch", hd_p_min: "Tối thiểu", hd_peek: "Giữ một mục để xem nó ẩn gì", hd_pick: "Chọn phần tử", hd_reset: "Đặt lại cho trang này", hd_rules: "Quy tắc của tôi", hd_title: "Ẩn", hide: "Ẩn (H)", hints: "M=ÂM F=TOÀN MH T=RẠP H=ẨN", hist_clear: "Xóa", hist_clear_q: "Xóa toàn bộ lịch sử xem?", hist_del: "Xóa khỏi lịch sử", hist_empty: "Hiện còn trống. Thời gian được tính khi video đang phát.", hist_s_last: "Gần đây", hist_s_time: "Thời gian", hist_s_vis: "Lượt xem", hist_title: "Lịch sử", hist_total: "Model: {n} · Tổng: {t}", hist_visits: "lượt", home: "Trang chủ", lang_auto: "Tự động", lang_title: "Ngôn ngữ", min: "Thu nhỏ", mv_abr_row: "Chất lượng tự động", mv_abr_t: "Chất lượng tự động: hạ chất lượng khi video giật hoặc mạng yếu và tăng lên khi kết nối ổn định", mv_add: "Thêm", mv_auto: "Tự động", mv_auto_all: "Tự động thêm tất cả luồng đang live", mv_back: "Quay lại", mv_bg: "Phát trong nền", mv_bg_t: "Luồng vẫn tiếp tục phát khi bạn chuyển tab, đóng cửa sổ này hoặc rời trình duyệt", mv_bm_h: "Đánh dấu đang live", mv_clear_q: "Đóng tất cả luồng?", mv_close: "Đóng", mv_cols: "Bố cục", mv_cur: "Luồng hiện tại", mv_dvr: "Bộ đệm tua lại", mv_dvr_off: "Tắt", mv_fs: "Toàn màn hình", mv_full: "Cửa sổ đã đầy", mv_go: "Thêm", mv_hint: "Thêm luồng để xem nhiều luồng cùng lúc. Chạm vào một ô để chuyển âm thanh sang ô đó.", mv_link_h: "Hoặc bằng liên kết", mv_load: "Đang tải…", mv_min_u: "phút", mv_mute: "Tắt tiếng", mv_none: "Không có đánh dấu đang live", mv_off: "Luồng không khả dụng", mv_pip: "Hình trong hình", mv_play: "Phát / tạm dừng", mv_qual: "Chất lượng", mv_retry: "chạm để thử lại", mv_search: "Tìm trong đánh dấu đang live", mv_title: "Trình phát", no_vid: "KHÔNG VIDEO", note_ph: "Ghi chú... (#thẻ)", pip: "PIP", pk_cancel: "Hủy", pk_found: "khớp", pk_hint: "Chạm vào một phần tử trên trang", pk_ok: "Ẩn", pk_up: "Lên", qual: "CHẤT LƯỢNG", rad_media: "Phương tiện", rad_nav: "Yêu thích", rad_next: "Sau", rad_page: "Trang", rad_prev: "Trước", rad_reset: "Đặt lại vị trí nút", rad_title: "Menu vòng tròn", rad_vdn: "Giảm âm", rad_view: "Hiển thị", rad_vup: "Tăng âm", rec: "GHI", rec_tip: "Chạm: ghi · Giữ: cài đặt", rel: "Tải lại", rs_cur: "Luồng hiện tại:", rs_gbh: "GB/giờ", rs_hint: "Khi một phần đạt kích thước đã chọn, nó được lưu thành tệp riêng và việc ghi tiếp tục ở phần mới. Dữ liệu được ghi vào ổ đĩa theo từng khối nhỏ; nếu ổ đĩa không khả dụng, dữ liệu nằm trong bộ nhớ điện thoại, khi đó nên chọn các phần nhỏ hơn.", rs_mbmin: "MB/phút", rs_next: "Thay đổi có hiệu lực từ phần tiếp theo hoặc bản ghi mới.", rs_now: "Hiện tại", rs_off: "Tắt", rs_p: "phần", rs_part: "Kích thước phần (tệp được chia)", rs_q_hi: "Cao", rs_q_low: "Thấp", rs_q_mid: "Vừa", rs_q_min: "Tiết kiệm", rs_quality: "Chất lượng (bitrate)", rs_recover: "Khôi phục bản ghi chưa lưu", rs_resnote: "Chế độ Capture ghi theo độ phân giải của luồng: các tùy chọn ở trên chỉ thay đổi bitrate (nén), còn 1080p–360p cho biết chất lượng hình ảnh dự kiến. Ở chế độ HLS, hệ thống chọn luồng có chiều cao không vượt quá mức đã chọn.", rs_src: "Nguồn", rs_src_cap: "Ghi hình (mã hóa lại)", rs_src_hls: "HLS (không mã hóa lại)", rs_src_note: "HLS tải các phân đoạn của luồng nguyên trạng, không mã hóa lại, và lưu thành MP4. Cần địa chỉ luồng; nếu không có, sẽ dùng ghi hình. Chất lượng là gốc, giống luồng nguồn. Bản ghi được ghi vào đĩa theo từng điểm kiểm tra nhỏ.", rs_t: "tổng", rs_title: "Cài đặt ghi hình", rs_total: "Dừng ở tổng dung lượng", rs_upto: "tối đa", search: "TÌM", seen_now: "vừa xong", seen_tip: "Online lần cuối", settings: "Cài đặt", sleep_custom: "Thời gian tùy chỉnh", sleep_hint: "Khi hết giờ, trình phát sẽ tắt và mọi bản ghi đang chạy sẽ dừng và được lưu. Chạm vào màn hình đen để bật lại.", sleep_left: "Còn lại", sleep_sec: "giây", sleep_start: "Bắt đầu", sleep_title: "Hẹn giờ tắt", sleep_wake: "Hẹn giờ đã kết thúc.\nChạm để tiếp tục", snap: "CHỤP", sort_date: "Ngày", sort_last: "Lần cuối", sort_live: "Live", sort_name: "Tên", sort_viewers: "Người xem", sound: "ÂM", st_av_refresh: "Làm mới ảnh đại diện", st_away: "BRB", st_back: "Quay lại", st_btns: "Nút trên bảng", st_btns_hint: "Chạm để ẩn nút hoặc hiện lại.", st_btns_reset: "Hiện tất cả nút", st_color: "Màu bảng điều khiển", st_color_rnd: "Ngẫu nhiên", st_cols: "Số cột", st_custom: "Màu tùy chỉnh", st_data: "Dữ liệu", st_fav: "Yêu thích", st_favnav: "Nút «trước / sau» trên bảng điều khiển", st_group: "Nhóm", st_hide_grp: "Ẩn lựa chọn nhóm", st_hide_note: "Ẩn ô ghi chú (thẻ)", st_hide_topic: "Ẩn mô tả model", st_hist: "Lịch sử xem", st_hist_on: "Lưu lịch sử", st_hue: "Sắc độ", st_lang: "Ngôn ngữ", st_lig: "Độ sáng", st_list: "Danh sách", st_look: "Giao diện", st_open: "Mở", st_prev: "Xem trước model", st_prev_off: "Không làm mới", st_prev_spd: "Làm mới xem trước", st_private: "Riêng tư", st_ptags: "Ẩn thẻ hồ sơ (BongaCams)", st_rec: "Ghi video", st_sat: "Độ bão hòa", st_scale: "Kích thước bảng", st_site_dark: "Giao diện tối của trang", st_star: "Ngôi sao trên ảnh thu nhỏ", st_vip: "VIP", star_add: "Thêm vào yêu thích", star_del: "Xóa khỏi yêu thích", star_del_q: "Người mẫu này có ghi chú hoặc nhóm. Xóa khỏi yêu thích?", thtr: "RẠP", tp_less: "[Ít hơn]", tp_more: "[Thêm]", u_d: "ngày", u_gb: "GB", u_h: "giờ", u_m: "phút", u_mb: "MB", viewers_tip: "Người xem trong phòng", w_new: "Chưa xem", w_seen: "Đã xem", }, TH: { ago: "{n}ที่แล้ว", bg: "พื้นหลัง", bk_csv: "ตาราง CSV", bk_csv_fav: "รายการโปรด", bk_csv_hist: "ประวัติ", bk_csv_ses: "เซสชัน", bk_exp: "ส่งออกข้อมูลสำรอง", bk_imp: "นำเข้าข้อมูลสำรอง", bk_p_fav: "รายการโปรด", bk_p_hist: "ประวัติ", bk_p_set: "การตั้งค่า", bk_parts: "เนื้อหาของข้อมูลสำรอง", bm_add: "เพิ่มในรายการโปรด", bm_all: "ทั้งหมด", bm_del: "ลบ", bm_dup: "เพิ่มไว้แล้ว!", bm_empty: "รายการว่างเปล่า", bm_find: "ค้นหาตามชื่อ…", bm_nomatch: "ไม่มีโมเดลที่ตรงกับตัวกรอง", bm_online: "ออนไลน์", bm_online_t: "แสดงเฉพาะออนไลน์", bm_pin: "ปักหมุดไว้บนสุด", bm_priv: "ส่วนตัว", bm_title: "รายการโปรด", bm_unpin: "เลิกปักหมุด", bt_hints: "คำแนะนำ", bt_stats: "ผู้ชม", bt_vol: "ระดับเสียง", cal_all_tag: "แท็กทั้งหมด", cal_allday: "ทั้งวัน", cal_c_dur: "เวลา", cal_c_model: "โมเดล", cal_c_n: "ครั้ง", cal_c_time: "เข้า", cal_hist: "ประวัติ", cal_loaded: "โหลดตารางแล้ว: {a}/{b}", cal_models: "โมเดล", cal_no_sched: "ตารางเวลาดึงมาจากโปรไฟล์ของรายการโปรด เปิดรายการโปรดแล้วรอสักครู่", cal_none: "ไม่มีรายการในวันนี้", cal_reset: "รีเซ็ต", cal_sched: "ตารางเวลา", cal_title: "ปฏิทิน", cal_today: "วันนี้", cal_total: "รวม", cal_watched: "ดูแล้ว", cf_cancel: "ยกเลิก", cf_ok: "ใช่", ch_avg: "ค่าเฉลี่ยรายวัน", ch_btn: "แผนภูมิ", ch_days: "เวลาต่อวัน 14 วัน", ch_hours: "กิจกรรมตามชั่วโมง", ch_peak: "สูงสุด", ch_top: "อันดับตามเวลา", copy: "ลิงก์", dark: "ธีมมืด / สว่าง", don_copied: "คัดลอกที่อยู่แล้ว", don_fail: "คัดลอกไม่สำเร็จ", don_sub: "ขอบคุณที่ใช้งาน", don_tap: "แตะที่อยู่เพื่อดูแบบเต็ม ปุ่มทางขวาใช้คัดลอก", don_tip: "สนับสนุนผู้สร้าง", don_title: "เลี้ยงกาแฟผู้สร้างสักแก้ว", drag: "⁞⁞ ควบคุม", err: "ผิดพลาด", favs: "รายการโปรด", fr_hold: "กดค้างเพื่อดูเฟรมปัจจุบัน", fr_none: "ไม่มีเฟรม: โมเดลออฟไลน์", fs: "เต็มจอ", grp_all: "ทุกกลุ่ม", grp_new: "+ กลุ่มใหม่…", grp_none: "ไม่มีกลุ่ม", hd_all: "ซ่อนทั้งหมด", hd_cats: "หมวดหมู่", hd_clear: "ลบกฎทั้งหมดของฉัน", hd_details: "รายละเอียด", hd_g_chat: "แชต", hd_g_site: "เว็บไซต์", hd_g_win: "ป๊อปอัป", hd_hidden: "ซ่อนแล้ว {n} จาก {m}", hd_na: "ยังไม่พบ", hd_none: "ยังไม่มีกฎ", hd_off: "แสดงทั้งหมด", hd_p_clean: "เครื่องเล่นสะอาด", hd_p_min: "น้อยที่สุด", hd_peek: "กดค้างที่รายการเพื่อดูว่าซ่อนอะไร", hd_pick: "เลือกองค์ประกอบ", hd_reset: "รีเซ็ตสำหรับเว็บนี้", hd_rules: "กฎของฉัน", hd_title: "การซ่อน", hide: "ซ่อน (H)", hints: "M=เสียง F=เต็มจอ T=โรงหนัง H=ซ่อน", hist_clear: "ล้าง", hist_clear_q: "ลบประวัติการรับชมทั้งหมดหรือไม่?", hist_del: "นำออกจากประวัติ", hist_empty: "ยังว่างอยู่ เวลาจะนับขณะที่วิดีโอเล่น", hist_s_last: "ล่าสุด", hist_s_time: "เวลา", hist_s_vis: "การเข้าชม", hist_title: "ประวัติ", hist_total: "โมเดล: {n} · รวม: {t}", hist_visits: "ครั้ง", home: "หน้าแรก", lang_auto: "อัตโนมัติ", lang_title: "ภาษา", min: "ย่อ", mv_abr_row: "คุณภาพอัตโนมัติ", mv_abr_t: "คุณภาพอัตโนมัติ: ลดคุณภาพเมื่อวิดีโอกระตุกหรือเครือข่ายอ่อน และเพิ่มขึ้นเมื่อการเชื่อมต่อเสถียร", mv_add: "เพิ่ม", mv_auto: "อัตโนมัติ", mv_auto_all: "เพิ่มสตรีมที่กำลังไลฟ์ทั้งหมดอัตโนมัติ", mv_back: "กลับ", mv_bg: "เล่นในพื้นหลัง", mv_bg_t: "สตรีมจะเล่นต่อเมื่อสลับแท็บ ปิดหน้าต่างนี้ หรือออกจากเบราว์เซอร์", mv_bm_h: "บุ๊กมาร์กที่กำลังไลฟ์", mv_clear_q: "ปิดทุกสตรีมหรือไม่", mv_close: "ปิด", mv_cols: "เลย์เอาต์", mv_cur: "สตรีมปัจจุบัน", mv_dvr: "บัฟเฟอร์ย้อนกลับ", mv_dvr_off: "ปิด", mv_fs: "เต็มจอ", mv_full: "หน้าต่างเต็มแล้ว", mv_go: "เพิ่ม", mv_hint: "เพิ่มสตรีมเพื่อดูหลายสตรีมพร้อมกัน แตะที่ช่องเพื่อย้ายเสียงไปที่ช่องนั้น", mv_link_h: "หรือผ่านลิงก์", mv_load: "กำลังโหลด…", mv_min_u: "นาที", mv_mute: "ปิดเสียง", mv_none: "ไม่มีบุ๊กมาร์กที่กำลังไลฟ์", mv_off: "สตรีมไม่พร้อมใช้งาน", mv_pip: "ภาพซ้อนภาพ", mv_play: "เล่น / หยุดชั่วคราว", mv_qual: "คุณภาพ", mv_retry: "แตะเพื่อลองใหม่", mv_search: "ค้นหาบุ๊กมาร์กที่กำลังไลฟ์", mv_title: "เครื่องเล่น", no_vid: "ไม่มีวิดีโอ", note_ph: "บันทึก... (#แท็ก)", pip: "PIP", pk_cancel: "ยกเลิก", pk_found: "ตรงกัน", pk_hint: "แตะองค์ประกอบบนหน้าเว็บ", pk_ok: "ซ่อน", pk_up: "ขึ้น", qual: "คุณภาพ", rad_media: "สื่อ", rad_nav: "รายการโปรด", rad_next: "ถัดไป", rad_page: "หน้า", rad_prev: "ก่อนหน้า", rad_reset: "รีเซ็ตตำแหน่งปุ่ม", rad_title: "เมนูวงกลม", rad_vdn: "เสียงเบาลง", rad_view: "มุมมอง", rad_vup: "เสียงดังขึ้น", rec: "อัด", rec_tip: "แตะ: อัด · กดค้าง: ตั้งค่า", rel: "โหลดใหม่", rs_cur: "สตรีมปัจจุบัน:", rs_gbh: "GB/ชม.", rs_hint: "เมื่อส่วนหนึ่งถึงขนาดที่เลือก ระบบจะบันทึกเป็นไฟล์แยก แล้วอัดต่อในส่วนใหม่ ข้อมูลถูกเขียนลงดิสก์เป็นก้อนเล็กๆ หากใช้ดิสก์ไม่ได้ ข้อมูลจะอยู่ในหน่วยความจำโทรศัพท์ ในกรณีนั้นควรเลือกส่วนที่เล็กลง", rs_mbmin: "MB/นาที", rs_next: "การเปลี่ยนแปลงมีผลตั้งแต่ส่วนถัดไปหรือการอัดครั้งใหม่", rs_now: "ตอนนี้", rs_off: "ปิด", rs_p: "ส่วน", rs_part: "ขนาดแต่ละส่วน (ไฟล์จะถูกแบ่ง)", rs_q_hi: "สูง", rs_q_low: "ต่ำ", rs_q_mid: "กลาง", rs_q_min: "ประหยัด", rs_quality: "คุณภาพ (บิตเรต)", rs_recover: "กู้คืนการบันทึกที่ยังไม่ได้บันทึก", rs_resnote: "โหมดแคปเจอร์อัดด้วยความละเอียดของสตรีม: ตัวเลือกด้านบนเปลี่ยนเฉพาะบิตเรต (การบีบอัด) ส่วน 1080p–360p แสดงคุณภาพภาพที่คาดไว้ ในโหมด HLS จะเลือกสตรีมที่สูงไม่เกินค่าที่กำหนด", rs_src: "แหล่งที่มา", rs_src_cap: "แคปเจอร์ (เข้ารหัสใหม่)", rs_src_hls: "HLS (ไม่เข้ารหัสใหม่)", rs_src_note: "HLS จะดาวน์โหลดส่วนของสตรีมตามที่เป็น โดยไม่เข้ารหัสใหม่ และบันทึกเป็น MP4 ต้องมีที่อยู่ของสตรีม หากไม่มีจะใช้แคปเจอร์ คุณภาพเป็นต้นฉบับ เหมือนสตรีมต้นทาง การบันทึกจะเขียนลงดิสก์เป็นจุดตรวจเล็ก ๆ", rs_t: "รวม", rs_title: "การตั้งค่าการอัด", rs_total: "หยุดเมื่อถึงขนาดรวม", rs_upto: "สูงสุด", search: "ค้นหา", seen_now: "เมื่อสักครู่", seen_tip: "ออนไลน์ล่าสุด", settings: "การตั้งค่า", sleep_custom: "กำหนดเวลาเอง", sleep_hint: "เมื่อหมดเวลา เครื่องเล่นจะปิด และการอัดที่กำลังทำอยู่จะหยุดและบันทึกไว้ แตะหน้าจอสีดำเพื่อเปิดกลับมา", sleep_left: "เหลือ", sleep_sec: "วินาที", sleep_start: "เริ่ม", sleep_title: "ตั้งเวลาปิด", sleep_wake: "ตั้งเวลาปิดสิ้นสุดแล้ว\nแตะเพื่อดำเนินการต่อ", snap: "แคป", sort_date: "วันที่", sort_last: "ล่าสุด", sort_live: "ไลฟ์", sort_name: "ชื่อ", sort_viewers: "ผู้ชม", sound: "เสียง", st_av_refresh: "รีเฟรชอวาตาร์", st_away: "BRB", st_back: "ย้อนกลับ", st_btns: "ปุ่มบนแผง", st_btns_hint: "แตะเพื่อซ่อนปุ่มหรือนำกลับมา", st_btns_reset: "แสดงปุ่มทั้งหมด", st_color: "สีของแผง", st_color_rnd: "สุ่ม", st_cols: "จำนวนคอลัมน์", st_custom: "สีกำหนดเอง", st_data: "ข้อมูล", st_fav: "รายการโปรด", st_favnav: "ปุ่ม «ก่อนหน้า / ถัดไป» บนแผงควบคุม", st_group: "กลุ่ม", st_hide_grp: "ซ่อนตัวเลือกกลุ่ม", st_hide_note: "ซ่อนช่องบันทึก (แท็ก)", st_hide_topic: "ซ่อนคำอธิบายโมเดล", st_hist: "ประวัติการรับชม", st_hist_on: "เก็บประวัติ", st_hue: "เฉดสี", st_lang: "ภาษา", st_lig: "ความสว่าง", st_list: "รายการ", st_look: "รูปลักษณ์", st_open: "เปิด", st_prev: "พรีวิวโมเดล", st_prev_off: "ไม่รีเฟรช", st_prev_spd: "การรีเฟรชพรีวิว", st_private: "ส่วนตัว", st_ptags: "ซ่อนแท็กโปรไฟล์ (BongaCams)", st_rec: "การบันทึกวิดีโอ", st_sat: "ความอิ่มตัว", st_scale: "ขนาดแผง", st_site_dark: "ธีมมืดของเว็บไซต์", st_star: "ดาวบนภาพขนาดย่อ", st_vip: "VIP", star_add: "เพิ่มในรายการโปรด", star_del: "นำออกจากรายการโปรด", star_del_q: "โมเดลนี้มีโน้ตหรือกลุ่ม นำออกจากรายการโปรดหรือไม่?", thtr: "โรงหนัง", tp_less: "[น้อยลง]", tp_more: "[เพิ่มเติม]", u_d: "วัน", u_gb: "GB", u_h: "ชม.", u_m: "นาที", u_mb: "MB", viewers_tip: "ผู้ชมในห้อง", w_new: "ยังไม่ได้ดู", w_seen: "ดูแล้ว", }, }; LANGS.forEach(k => { i18n[k] = Object.assign({}, i18n.EN, i18n[k]); }); const HIDE_L = { UK: { actions: ["Кнопки «Приват / Чайові»","Сині (на ПК зелені) кнопки під червоною панеллю"], age18: ["Вікно «Старше 18»","Заставка «Заходь, ТІЛЬКИ якщо тобі вже є 18»"], balance: ["Баланс і меню (ПК)","Баланс і навігація на комп’ютері"], below: ["Панель під плеєром (ПК)","Блок кнопок під відео"], cb_actions: ["Кнопка чайових","Кнопка та форма надсилання токенів"], cb_alerts: ["Сповіщення","Попередження про мову та нотатки, що закриваються"], cb_footer: ["Низ сторінки","Футер і соцмережі"], cb_hdr: ["Верх сайту","Шапка з логотипом і меню"], cb_info: ["Назва кімнати","Імʼя та лічильник глядачів у шапці"], cb_sort: ["Сортування","Панель сортування в мобільній версії"], cb_status: ["Статус кімнати","Плашка про офлайн і приват з підказками"], cb_tags: ["Смуга тегів","Популярні теги над списком"], cb_tips: ["Чайові в чаті","Повідомлення про токени та чайові"], chat_boost: ["Бусти в чаті","Плашки «підняв модель на N секунд» (FAN BOOSTED)"], chat_tips: ["Чайові в чаті","Повідомлення «подарував токени», меню чайових"], chat_topic: ["Тема кімнати","Довгий опис «Тема: …» на початку чату"], chathdr: ["Плашка «Чат із …»","Рядок «Безкоштовний чат із …» і король чайових"], ftr: ["Низ сторінки","Футер, соцмережі, SEO-текст"], games: ["Кнопка «Ігри»","Іконка геймпада в червоній панелі"], hdr_d: ["Шапка сайту (ПК)","Верхня панель на комп’ютері"], hdr_m: ["Верх сайту","Логотип, меню, кнопки входу"], joinpop: ["Вікно реєстрації","Спливаюча форма «Створіть акаунт»"], lastview: ["Плашка «Продовжити перегляд»","Кнопка на превʼю останньої моделі"], modelchat: ["Спливаючий чат моделі","Рожеве віконце з автопривітанням моделі внизу"], popups: ["Спливаючі вікна","Вибір чату, група, підглядання"], promo: ["Реклама та банери","Банери, промо, плашка «Приєднуйся» на плеєрі, застосунок"], sc_about: ["Блоки під плеєром","Про мене, розклад, схожі моделі"], sc_cookie: ["Плашка cookies","Сповіщення про cookie внизу"], sc_footer: ["Низ сторінки","Футер і юридичний текст"], sc_goal: ["Ціль (гол)","Смуга прогресу цілі"], sc_hdr: ["Верх сайту","Шапка з логотипом, пошуком і кнопками входу"], sc_king: ["Король чайових","Плашка короля чайових"], sc_promo: ["Реклама та банери","Розіграші токенів, кнопка «Приєднатися» на плеєрі"], sc_sub: ["Рядок моделі","Аватар, підписка та меню над плеєром"], sc_topic: ["Тема кімнати","Текст теми над плеєром і всередині нього"], sc_widgets: ["Віджети на плеєрі","Опитування та реакції поверх відео"], sc_wm: ["Водяний знак","Логотип поверх відео"], seotitle: ["Заголовок-реклама","Рядок із SEO-заголовком над списком"], tabs: ["Вкладки та сортування","Дівчата/пари, фільтри, значок сортування"], tipking: ["Король чайових","Рядок короля та вікна чайових"], tips: ["Підказки в чаті","Спливаючі підказки та сповіщення"], wm: ["Водяний знак (ПК)","Логотип поверх відео"], }, ES: { actions: ["Botones «Privado / Propinas»","Botones azules (verdes en PC) bajo la barra roja"], age18: ["Aviso +18","Pantalla de entrada «Solo mayores de 18»"], balance: ["Saldo y menú (PC)","Saldo y navegación en ordenador"], below: ["Barra bajo el reproductor (PC)","Fila de botones bajo el vídeo"], cb_actions: ["Botón de propina","Botón y formulario para enviar tokens"], cb_alerts: ["Avisos","Aviso de idioma y notas descartables"], cb_footer: ["Pie de página","Pie y redes sociales"], cb_hdr: ["Parte superior del sitio","Cabecera con logo y menú"], cb_info: ["Nombre de la sala","Nombre y contador de espectadores en la cabecera"], cb_sort: ["Orden","Panel de orden en la versión móvil"], cb_status: ["Estado de la sala","Aviso de desconexión y privado con sugerencias"], cb_tags: ["Franja de etiquetas","Etiquetas populares sobre la lista"], cb_tips: ["Propinas en el chat","Mensajes de tokens y propinas"], chat_boost: ["Boosts en el chat","Tarjetas de «impulsó a la modelo N segundos»"], chat_tips: ["Propinas en el chat","Mensajes de «dio tokens» y menú de propinas"], chat_topic: ["Tema de la sala","Bloque largo «Tema: …» al inicio del chat"], chathdr: ["Barra «Chat con …»","Franja «Chat gratis con …» y rey de propinas"], ftr: ["Pie de página","Pie, redes sociales, texto SEO"], games: ["Botón «Juegos»","Icono de mando en la barra roja"], hdr_d: ["Cabecera del sitio (PC)","Barra superior en ordenador"], hdr_m: ["Parte superior del sitio","Logo, menú, botones de acceso"], joinpop: ["Ventana de registro","Formulario emergente «Crea una cuenta»"], lastview: ["Marca «Seguir viendo»","Botón sobre la miniatura de la última modelo"], modelchat: ["Chat emergente de la modelo","Ventana rosa con el saludo automático abajo"], popups: ["Ventanas emergentes","Elección de chat, grupo, voyeur"], promo: ["Anuncios y banners","Banners, promos, franja «Únete» sobre el reproductor, app"], sc_about: ["Bloques bajo el reproductor","Sobre mí, horario, modelos similares"], sc_cookie: ["Aviso de cookies","Aviso de cookies en la parte inferior"], sc_footer: ["Pie de página","Pie y texto legal"], sc_goal: ["Objetivo (goal)","Barra de progreso del objetivo"], sc_hdr: ["Parte superior del sitio","Cabecera con logo, búsqueda y botones de acceso"], sc_king: ["Rey de propinas","Franja del rey de propinas"], sc_promo: ["Anuncios y banners","Sorteos de tokens, botón «Unirse» en el reproductor"], sc_sub: ["Barra de la modelo","Avatar, seguir y menú sobre el reproductor"], sc_topic: ["Tema de la sala","Texto del tema sobre y dentro del reproductor"], sc_widgets: ["Widgets del reproductor","Encuestas y reacciones sobre el vídeo"], sc_wm: ["Marca de agua","Logo sobre el vídeo"], seotitle: ["Título publicitario","Línea de título SEO sobre la lista"], tabs: ["Pestañas y orden","Chicas/parejas, filtros, icono de orden"], tipking: ["Rey de propinas","Franja del rey y ventanas de propinas"], tips: ["Consejos del chat","Avisos y notificaciones emergentes"], wm: ["Marca de agua (PC)","Logo sobre el vídeo"], }, DE: { actions: ["Buttons „Privat / Trinkgeld“","Blaue (am PC grüne) Buttons unter der roten Leiste"], age18: ["18+-Hinweis","Einstiegsbildschirm „Nur ab 18“"], balance: ["Guthaben & Menü (PC)","Guthaben und Navigation am Computer"], below: ["Leiste unter dem Player (PC)","Button-Reihe unter dem Video"], cb_actions: ["Trinkgeld-Button","Button und Formular zum Senden von Tokens"], cb_alerts: ["Hinweise","Sprachhinweis und schließbare Notizen"], cb_footer: ["Seitenende","Footer und Social Media"], cb_hdr: ["Seitenkopf","Kopfzeile mit Logo und Menü"], cb_info: ["Raumname","Name und Zuschauerzahl in der Kopfzeile"], cb_sort: ["Sortierung","Sortierleiste in der mobilen Ansicht"], cb_status: ["Raumstatus","Hinweis zu Offline und Privat mit Tipps"], cb_tags: ["Tag-Leiste","Beliebte Tags über der Liste"], cb_tips: ["Trinkgelder im Chat","Meldungen zu Tokens und Trinkgeld"], chat_boost: ["Boosts im Chat","Karten „hat das Model N Sekunden geboostet“"], chat_tips: ["Trinkgelder im Chat","Meldungen „hat Tokens gegeben“, Tip-Menü"], chat_topic: ["Raumthema","Langer Block „Thema: …“ am Chatanfang"], chathdr: ["Leiste „Chat mit …“","Streifen „Gratis-Chat mit …“ und Tip-König"], ftr: ["Seitenende","Footer, Social Media, SEO-Text"], games: ["Button „Spiele“","Gamepad-Symbol in der roten Leiste"], hdr_d: ["Seitenkopf (PC)","Obere Leiste am Computer"], hdr_m: ["Seitenkopf","Logo, Menü, Anmelde-Buttons"], joinpop: ["Anmelde-Fenster","Popup „Konto erstellen“"], lastview: ["Hinweis „Weiter ansehen“","Button auf der Vorschau des zuletzt gesehenen Models"], modelchat: ["Pop-up-Chat des Models","Rosa Fenster mit der Auto-Begrüßung unten"], popups: ["Pop-up-Fenster","Chat-Auswahl, Gruppe, Voyeur"], promo: ["Werbung & Banner","Banner, Promos, „Mach mit“-Leiste am Player, App"], sc_about: ["Bereiche unter dem Player","Über mich, Zeitplan, ähnliche Models"], sc_cookie: ["Cookie-Hinweis","Cookie-Hinweis am unteren Rand"], sc_footer: ["Seitenende","Footer und rechtliche Texte"], sc_goal: ["Ziel (Goal)","Fortschrittsbalken des Ziels"], sc_hdr: ["Seitenkopf","Kopfzeile mit Logo, Suche und Anmelde-Buttons"], sc_king: ["Tip-König","Streifen des Tip-Königs"], sc_promo: ["Werbung & Banner","Token-Verlosungen, „Beitreten“-Button am Player"], sc_sub: ["Model-Leiste","Avatar, Folgen und Menü über dem Player"], sc_topic: ["Raumthema","Themen-Text über und im Player"], sc_widgets: ["Player-Widgets","Umfragen und Reaktionen über dem Video"], sc_wm: ["Wasserzeichen","Logo über dem Video"], seotitle: ["Werbe-Überschrift","SEO-Überschrift über der Liste"], tabs: ["Tabs & Sortierung","Mädchen/Paare, Filter, Sortier-Symbol"], tipking: ["Tip-König","Königs-Streifen und Trinkgeld-Fenster"], tips: ["Chat-Hinweise","Tooltips und Benachrichtigungen"], wm: ["Wasserzeichen (PC)","Logo über dem Video"], }, FR: { actions: ["Boutons « Privé / Pourboires »","Boutons bleus (verts sur PC) sous la barre rouge"], age18: ["Avis 18+","Écran d’entrée « Réservé aux plus de 18 ans »"], balance: ["Solde et menu (PC)","Solde et navigation sur ordinateur"], below: ["Barre sous le lecteur (PC)","Rangée de boutons sous la vidéo"], cb_actions: ["Bouton de pourboire","Bouton et formulaire d’envoi de tokens"], cb_alerts: ["Notifications","Alerte de langue et notes fermables"], cb_footer: ["Bas de page","Pied de page et réseaux sociaux"], cb_hdr: ["Haut du site","En-tête avec logo et menu"], cb_info: ["Nom du salon","Nom et nombre de spectateurs dans l’en-tête"], cb_sort: ["Tri","Panneau de tri sur mobile"], cb_status: ["Statut du salon","Bandeau hors ligne et privé avec astuces"], cb_tags: ["Bandeau de tags","Tags populaires au-dessus de la liste"], cb_tips: ["Pourboires dans le chat","Messages de tokens et de pourboires"], chat_boost: ["Boosts dans le chat","Cartes « a boosté la modèle N secondes »"], chat_tips: ["Pourboires dans le chat","Messages « a donné des tokens », menu de pourboires"], chat_topic: ["Sujet du salon","Long bloc « Sujet : … » en haut du chat"], chathdr: ["Barre « Chat avec … »","Bandeau « Chat gratuit avec … » et roi des pourboires"], ftr: ["Bas de page","Pied de page, réseaux sociaux, texte SEO"], games: ["Bouton « Jeux »","Icône de manette dans la barre rouge"], hdr_d: ["En-tête du site (PC)","Barre supérieure sur ordinateur"], hdr_m: ["Haut du site","Logo, menu, boutons de connexion"], joinpop: ["Fenêtre d’inscription","Formulaire « Créez un compte »"], lastview: ["Pastille « Reprendre »","Bouton sur la miniature du dernier modèle vu"], modelchat: ["Chat flottant de la modèle","Fenêtre rose avec le message d’accueil automatique, en bas"], popups: ["Fenêtres surgissantes","Choix de chat, groupe, voyeur"], promo: ["Pubs et bannières","Bannières, promos, bandeau « Rejoins-moi » sur le lecteur, appli"], sc_about: ["Blocs sous le lecteur","À propos, planning, modèles similaires"], sc_cookie: ["Bandeau cookies","Avis sur les cookies en bas"], sc_footer: ["Bas de page","Pied de page et mentions légales"], sc_goal: ["Objectif (goal)","Barre de progression de l’objectif"], sc_hdr: ["Haut du site","En-tête avec logo, recherche et boutons de connexion"], sc_king: ["Roi des pourboires","Bandeau du roi des pourboires"], sc_promo: ["Pubs et bannières","Tirages de tokens, bouton « Rejoindre » sur le lecteur"], sc_sub: ["Barre du modèle","Avatar, abonnement et menu au-dessus du lecteur"], sc_topic: ["Sujet du salon","Texte du sujet au-dessus et dans le lecteur"], sc_widgets: ["Widgets du lecteur","Sondages et réactions sur la vidéo"], sc_wm: ["Filigrane","Logo sur la vidéo"], seotitle: ["Titre publicitaire","Ligne de titre SEO au-dessus de la liste"], tabs: ["Onglets et tri","Filles/couples, filtres, icône de tri"], tipking: ["Roi des pourboires","Bandeau du roi et fenêtres de pourboires"], tips: ["Astuces du chat","Infobulles et notifications"], wm: ["Filigrane (PC)","Logo sur la vidéo"], }, PT: { actions: ["Botões «Privado / Gorjetas»","Botões azuis (verdes no PC) sob a barra vermelha"], age18: ["Aviso 18+","Tela de entrada «Somente maiores de 18»"], balance: ["Saldo e menu (PC)","Saldo e navegação no computador"], below: ["Barra sob o player (PC)","Fileira de botões sob o vídeo"], cb_actions: ["Botão de gorjeta","Botão e formulário para enviar tokens"], cb_alerts: ["Avisos","Aviso de idioma e notas dispensáveis"], cb_footer: ["Rodapé","Rodapé e redes sociais"], cb_hdr: ["Topo do site","Cabeçalho com logo e menu"], cb_info: ["Nome da sala","Nome e contador de espectadores no cabeçalho"], cb_sort: ["Ordenação","Painel de ordenação na versão móvel"], cb_status: ["Status da sala","Aviso de offline e privado com dicas"], cb_tags: ["Faixa de tags","Tags populares acima da lista"], cb_tips: ["Gorjetas no chat","Mensagens de tokens e gorjetas"], chat_boost: ["Boosts no chat","Cartões de «impulsionou a modelo por N segundos»"], chat_tips: ["Gorjetas no chat","Mensagens de «deu tokens» e menu de gorjetas"], chat_topic: ["Tema da sala","Bloco longo «Tema: …» no início do chat"], chathdr: ["Barra «Chat com …»","Faixa «Chat grátis com …» e rei das gorjetas"], ftr: ["Rodapé","Rodapé, redes sociais, texto SEO"], games: ["Botão «Jogos»","Ícone de controle na barra vermelha"], hdr_d: ["Cabeçalho do site (PC)","Barra superior no computador"], hdr_m: ["Topo do site","Logo, menu, botões de login"], joinpop: ["Janela de cadastro","Pop-up «Crie uma conta»"], lastview: ["Selo «Continuar assistindo»","Botão na miniatura da última modelo vista"], modelchat: ["Chat pop-up da modelo","Janela rosa com a saudação automática, embaixo"], popups: ["Janelas pop-up","Escolha de chat, grupo, voyeur"], promo: ["Anúncios e banners","Banners, promoções, faixa «Junte-se» no player, app"], sc_about: ["Blocos abaixo do player","Sobre mim, agenda, modelos semelhantes"], sc_cookie: ["Aviso de cookies","Aviso de cookies na parte inferior"], sc_footer: ["Rodapé","Rodapé e texto legal"], sc_goal: ["Meta (goal)","Barra de progresso da meta"], sc_hdr: ["Topo do site","Cabeçalho com logo, busca e botões de login"], sc_king: ["Rei das gorjetas","Faixa do rei das gorjetas"], sc_promo: ["Anúncios e banners","Sorteios de tokens, botão «Participar» no player"], sc_sub: ["Barra da modelo","Avatar, seguir e menu acima do player"], sc_topic: ["Tema da sala","Texto do tema acima e dentro do player"], sc_widgets: ["Widgets do player","Enquetes e reações sobre o vídeo"], sc_wm: ["Marca d’água","Logo sobre o vídeo"], seotitle: ["Título publicitário","Linha de título SEO acima da lista"], tabs: ["Abas e ordenação","Garotas/casais, filtros, ícone de ordenação"], tipking: ["Rei das gorjetas","Faixa do rei e janelas de gorjetas"], tips: ["Dicas do chat","Dicas e notificações pop-up"], wm: ["Marca d’água (PC)","Logo sobre o vídeo"], }, IT: { actions: ["Pulsanti «Privato / Mance»","Pulsanti blu (verdi su PC) sotto la barra rossa"], age18: ["Avviso 18+","Schermata d’ingresso «Solo maggiorenni»"], balance: ["Saldo e menu (PC)","Saldo e navigazione su computer"], below: ["Barra sotto il player (PC)","Fila di pulsanti sotto il video"], cb_actions: ["Pulsante mancia","Pulsante e modulo per inviare token"], cb_alerts: ["Avvisi","Avviso sulla lingua e note chiudibili"], cb_footer: ["Fondo pagina","Footer e social"], cb_hdr: ["Parte alta del sito","Intestazione con logo e menu"], cb_info: ["Nome della stanza","Nome e contatore spettatori nell’intestazione"], cb_sort: ["Ordinamento","Pannello di ordinamento nella versione mobile"], cb_status: ["Stato della stanza","Avviso su offline e privato con suggerimenti"], cb_tags: ["Striscia dei tag","Tag popolari sopra l’elenco"], cb_tips: ["Mance in chat","Messaggi su token e mance"], chat_boost: ["Boost in chat","Schede «ha potenziato la modella per N secondi»"], chat_tips: ["Mance in chat","Messaggi «ha dato token» e menu mance"], chat_topic: ["Argomento della stanza","Lungo blocco «Argomento: …» all’inizio della chat"], chathdr: ["Barra «Chat con …»","Striscia «Chat gratis con …» e re delle mance"], ftr: ["Fondo pagina","Footer, social, testo SEO"], games: ["Pulsante «Giochi»","Icona del gamepad nella barra rossa"], hdr_d: ["Intestazione del sito (PC)","Barra superiore su computer"], hdr_m: ["Parte alta del sito","Logo, menu, pulsanti di accesso"], joinpop: ["Finestra di registrazione","Popup «Crea un account»"], lastview: ["Badge «Continua a guardare»","Pulsante sull’anteprima dell’ultima modella vista"], modelchat: ["Chat popup della modella","Finestra rosa con il saluto automatico, in basso"], popups: ["Finestre popup","Scelta chat, gruppo, voyeur"], promo: ["Pubblicità e banner","Banner, promo, striscia «Unisciti» sul player, app"], sc_about: ["Blocchi sotto il player","Chi sono, programma, modelle simili"], sc_cookie: ["Avviso cookie","Avviso sui cookie in basso"], sc_footer: ["Fondo pagina","Footer e testo legale"], sc_goal: ["Obiettivo (goal)","Barra di avanzamento dell’obiettivo"], sc_hdr: ["Parte alta del sito","Intestazione con logo, ricerca e pulsanti di accesso"], sc_king: ["Re delle mance","Striscia del re delle mance"], sc_promo: ["Pubblicità e banner","Estrazioni di token, pulsante «Unisciti» sul player"], sc_sub: ["Barra della modella","Avatar, segui e menu sopra il player"], sc_topic: ["Argomento della stanza","Testo dell’argomento sopra e dentro il player"], sc_widgets: ["Widget del player","Sondaggi e reazioni sopra il video"], sc_wm: ["Filigrana","Logo sopra il video"], seotitle: ["Titolo pubblicitario","Riga di titolo SEO sopra l’elenco"], tabs: ["Schede e ordinamento","Ragazze/coppie, filtri, icona di ordinamento"], tipking: ["Re delle mance","Striscia del re e finestre delle mance"], tips: ["Suggerimenti della chat","Tooltip e notifiche"], wm: ["Filigrana (PC)","Logo sopra il video"], }, PL: { actions: ["Przyciski „Prywatny / Napiwki”","Niebieskie (na PC zielone) przyciski pod czerwonym paskiem"], age18: ["Ostrzeżenie 18+","Ekran wejściowy „Tylko dla pełnoletnich”"], balance: ["Saldo i menu (PC)","Saldo i nawigacja na komputerze"], below: ["Pasek pod odtwarzaczem (PC)","Rząd przycisków pod wideo"], cb_actions: ["Przycisk napiwku","Przycisk i formularz wysyłania tokenów"], cb_alerts: ["Powiadomienia","Ostrzeżenie o języku i zamykane notatki"], cb_footer: ["Dół strony","Stopka i social media"], cb_hdr: ["Góra strony","Nagłówek z logo i menu"], cb_info: ["Nazwa pokoju","Nazwa i licznik widzów w nagłówku"], cb_sort: ["Sortowanie","Panel sortowania w wersji mobilnej"], cb_status: ["Status pokoju","Plakietka offline i prywatny z podpowiedziami"], cb_tags: ["Pasek tagów","Popularne tagi nad listą"], cb_tips: ["Napiwki na czacie","Wiadomości o tokenach i napiwkach"], chat_boost: ["Boosty na czacie","Karty „podbił modelkę na N sekund”"], chat_tips: ["Napiwki na czacie","Wiadomości „dał tokeny”, menu napiwków"], chat_topic: ["Temat pokoju","Długi blok „Temat: …” na początku czatu"], chathdr: ["Pasek „Czat z …”","Pasek „Darmowy czat z …” i król napiwków"], ftr: ["Dół strony","Stopka, social media, tekst SEO"], games: ["Przycisk „Gry”","Ikona pada w czerwonym pasku"], hdr_d: ["Nagłówek strony (PC)","Górny pasek na komputerze"], hdr_m: ["Góra strony","Logo, menu, przyciski logowania"], joinpop: ["Okno rejestracji","Wyskakujący formularz „Załóż konto”"], lastview: ["Plakietka „Kontynuuj oglądanie”","Przycisk na miniaturze ostatniej modelki"], modelchat: ["Wyskakujący czat modelki","Różowe okienko z automatycznym powitaniem na dole"], popups: ["Okna wyskakujące","Wybór czatu, grupa, podglądanie"], promo: ["Reklamy i banery","Banery, promocje, pasek „Dołącz” na odtwarzaczu, aplikacja"], sc_about: ["Bloki pod odtwarzaczem","O mnie, harmonogram, podobne modelki"], sc_cookie: ["Komunikat o cookies","Informacja o cookies na dole"], sc_footer: ["Dół strony","Stopka i teksty prawne"], sc_goal: ["Cel (goal)","Pasek postępu celu"], sc_hdr: ["Góra strony","Nagłówek z logo, wyszukiwarką i przyciskami logowania"], sc_king: ["Król napiwków","Pasek króla napiwków"], sc_promo: ["Reklamy i banery","Losowania tokenów, przycisk „Dołącz” na odtwarzaczu"], sc_sub: ["Pasek modelki","Awatar, obserwowanie i menu nad odtwarzaczem"], sc_topic: ["Temat pokoju","Tekst tematu nad odtwarzaczem i w nim"], sc_widgets: ["Widżety odtwarzacza","Ankiety i reakcje nad wideo"], sc_wm: ["Znak wodny","Logo na wideo"], seotitle: ["Reklamowy nagłówek","Linia z nagłówkiem SEO nad listą"], tabs: ["Zakładki i sortowanie","Dziewczyny/pary, filtry, ikona sortowania"], tipking: ["Król napiwków","Pasek króla i okna napiwków"], tips: ["Podpowiedzi czatu","Dymki i powiadomienia"], wm: ["Znak wodny (PC)","Logo na wideo"], }, TR: { actions: ["«Özel / Bahşiş» düğmeleri","Kırmızı çubuğun altındaki mavi (PC’de yeşil) düğmeler"], age18: ["18+ uyarısı","«Yalnızca 18 yaş üstü» giriş ekranı"], balance: ["Bakiye ve menü (PC)","Bilgisayarda bakiye ve gezinme"], below: ["Oynatıcı altı çubuk (PC)","Videonun altındaki düğme sırası"], cb_actions: ["Bahşiş düğmesi","Token gönderme düğmesi ve formu"], cb_alerts: ["Bildirimler","Dil uyarısı ve kapatılabilir notlar"], cb_footer: ["Sayfa altı","Alt bilgi ve sosyal ağlar"], cb_hdr: ["Sitenin üst kısmı","Logo ve menü olan başlık"], cb_info: ["Oda adı","Başlıktaki ad ve izleyici sayacı"], cb_sort: ["Sıralama","Mobil sürümde sıralama paneli"], cb_status: ["Oda durumu","Çevrimdışı ve özel şeridi ile ipuçları"], cb_tags: ["Etiket şeridi","Listenin üstündeki popüler etiketler"], cb_tips: ["Sohbette bahşişler","Token ve bahşiş mesajları"], chat_boost: ["Sohbette boostlar","«Modeli N saniye öne çıkardı» kartları"], chat_tips: ["Sohbette bahşişler","«Token verdi» mesajları ve bahşiş menüsü"], chat_topic: ["Oda konusu","Sohbetin başındaki uzun «Konu: …» bloğu"], chathdr: ["«… ile sohbet» çubuğu","«… ile ücretsiz sohbet» şeridi ve bahşiş kralı"], ftr: ["Sayfa altı","Alt bilgi, sosyal ağlar, SEO metni"], games: ["«Oyunlar» düğmesi","Kırmızı çubuktaki oyun kolu simgesi"], hdr_d: ["Site başlığı (PC)","Bilgisayarda üst çubuk"], hdr_m: ["Sitenin üst kısmı","Logo, menü, giriş düğmeleri"], joinpop: ["Kayıt penceresi","«Hesap oluştur» açılır formu"], lastview: ["«İzlemeye devam et» rozeti","Son bakılan modelin önizlemesindeki düğme"], modelchat: ["Modelin açılır sohbeti","Altta otomatik karşılama mesajlı pembe pencere"], popups: ["Açılır pencereler","Sohbet seçimi, grup, gözetleme"], promo: ["Reklamlar ve bannerlar","Bannerlar, promolar, oynatıcıdaki «Katıl» şeridi, uygulama"], sc_about: ["Oynatıcı altı bloklar","Hakkımda, program, benzer modeller"], sc_cookie: ["Çerez uyarısı","Altta çerez bildirimi"], sc_footer: ["Sayfa altı","Alt bilgi ve yasal metin"], sc_goal: ["Hedef (goal)","Hedef ilerleme çubuğu"], sc_hdr: ["Sitenin üst kısmı","Logo, arama ve giriş düğmeleri olan başlık"], sc_king: ["Bahşiş kralı","Bahşiş kralı şeridi"], sc_promo: ["Reklamlar ve bannerlar","Token çekilişleri, oynatıcıdaki «Katıl» düğmesi"], sc_sub: ["Model çubuğu","Oynatıcının üstünde avatar, takip ve menü"], sc_topic: ["Oda konusu","Oynatıcının üstündeki ve içindeki konu metni"], sc_widgets: ["Oynatıcı widget’ları","Videonun üstünde anketler ve tepkiler"], sc_wm: ["Filigran","Videonun üzerindeki logo"], seotitle: ["Reklam başlığı","Listenin üstündeki SEO başlık satırı"], tabs: ["Sekmeler ve sıralama","Kızlar/çiftler, filtreler, sıralama simgesi"], tipking: ["Bahşiş kralı","Kral şeridi ve bahşiş pencereleri"], tips: ["Sohbet ipuçları","Açılır ipuçları ve bildirimler"], wm: ["Filigran (PC)","Videonun üzerindeki logo"], }, NL: { actions: ["Knoppen „Privé / Tip”","Blauwe (op pc groene) knoppen onder de rode balk"], age18: ["18+-melding","Startscherm „Alleen 18+”"], balance: ["Saldo en menu (pc)","Saldo en navigatie op de computer"], below: ["Balk onder de speler (pc)","Knoppenrij onder de video"], cb_actions: ["Tipknop","Knop en formulier om tokens te sturen"], cb_alerts: ["Meldingen","Taalwaarschuwing en sluitbare notities"], cb_footer: ["Voettekst","Footer en socials"], cb_hdr: ["Site-header","Header met logo en menu"], cb_info: ["Kamernaam","Naam en kijkersteller in de header"], cb_sort: ["Sortering","Sorteerpaneel in de mobiele versie"], cb_status: ["Kamerstatus","Offline- en privémelding met hints"], cb_tags: ["Tagbalk","Populaire tags boven de lijst"], cb_tips: ["Tips in de chat","Berichten over tokens en tips"], chat_boost: ["Boosts in de chat","Kaarten „heeft het model N seconden geboost”"], chat_tips: ["Tips in de chat","Meldingen „gaf tokens”, tipmenu"], chat_topic: ["Kamerthema","Lang blok „Thema: …” aan het begin van de chat"], chathdr: ["Balk „Chat met …”","Strook „Gratis chat met …” en tipkoning"], ftr: ["Voettekst","Footer, socials, SEO-tekst"], games: ["Knop „Spelletjes”","Gamepad-icoon in de rode balk"], hdr_d: ["Site-header (pc)","Bovenbalk op de computer"], hdr_m: ["Site-header","Logo, menu, inlogknoppen"], joinpop: ["Aanmeldvenster","Pop-up „Account maken”"], lastview: ["Melding „Verder kijken”","Knop op de preview van het laatst bekeken model"], modelchat: ["Pop-upchat van model","Roze venster met automatische begroeting onderaan"], popups: ["Pop-upvensters","Chatkeuze, groep, voyeur"], promo: ["Reclame en banners","Banners, promo's, „Doe mee”-balk op de speler, app"], sc_about: ["Blokken onder de speler","Over mij, schema, vergelijkbare modellen"], sc_cookie: ["Cookiemelding","Cookiemelding onderaan"], sc_footer: ["Voettekst","Footer en juridische tekst"], sc_goal: ["Doel (goal)","Voortgangsbalk van het doel"], sc_hdr: ["Site-header","Header met logo, zoekfunctie en inlogknoppen"], sc_king: ["Tipkoning","Strook van de tipkoning"], sc_promo: ["Reclame en banners","Tokenverlotingen, «Doe mee»-knop op de speler"], sc_sub: ["Modelbalk","Avatar, volgen en menu boven de speler"], sc_topic: ["Kamerthema","Themateksten boven en in de speler"], sc_widgets: ["Speler-widgets","Polls en reacties over de video"], sc_wm: ["Watermerk","Logo over de video"], seotitle: ["Reclamekop","SEO-kop boven de lijst"], tabs: ["Tabs en sortering","Meisjes/koppels, filters, sorteericoon"], tipking: ["Tipkoning","Koningsstrook en tipvenster"], tips: ["Chat-hints","Tooltips en meldingen"], wm: ["Watermerk (pc)","Logo over de video"], }, CS: { actions: ["Tlačítka „Privát / Tip“","Modrá (na PC zelená) tlačítka pod červenou lištou"], age18: ["Upozornění 18+","Úvodní obrazovka „Pouze 18+“"], balance: ["Zůstatek a menu (PC)","Zůstatek a navigace na počítači"], below: ["Lišta pod přehrávačem (PC)","Řada tlačítek pod videem"], cb_actions: ["Tlačítko tipu","Tlačítko a formulář pro odeslání tokenů"], cb_alerts: ["Oznámení","Upozornění na jazyk a zavíratelné poznámky"], cb_footer: ["Patička","Patička a sociální sítě"], cb_hdr: ["Hlavička webu","Hlavička s logem a menu"], cb_info: ["Název místnosti","Jméno a počet diváků v hlavičce"], cb_sort: ["Řazení","Panel řazení v mobilní verzi"], cb_status: ["Stav místnosti","Lišta offline a privát s nápovědami"], cb_tags: ["Pruh štítků","Oblíbené štítky nad seznamem"], cb_tips: ["Tipy v chatu","Zprávy o tokenech a tipech"], chat_boost: ["Boosty v chatu","Karty „boostl modelku na N sekund“"], chat_tips: ["Tipy v chatu","Zprávy „dal tokeny“, menu tipů"], chat_topic: ["Téma místnosti","Dlouhý blok „Téma: …“ na začátku chatu"], chathdr: ["Lišta „Chat s …“","Pruh „Zdarma chat s …“ a král tipů"], ftr: ["Patička","Patička, sítě, SEO text"], games: ["Tlačítko „Hry“","Ikona gamepadu v červené liště"], hdr_d: ["Hlavička webu (PC)","Horní panel na počítači"], hdr_m: ["Hlavička webu","Logo, menu, tlačítka přihlášení"], joinpop: ["Registrační okno","Vyskakovací formulář „Vytvořit účet“"], lastview: ["Lišta „Pokračovat ve sledování“","Tlačítko na náhledu poslední modelky"], modelchat: ["Vyskakovací chat modelky","Růžové okno s automatickým pozdravem dole"], popups: ["Vyskakovací okna","Výběr chatu, skupina, voyeur"], promo: ["Reklama a bannery","Bannery, promo, lišta „Připoj se“ na přehrávači, aplikace"], sc_about: ["Bloky pod přehrávačem","O mně, rozvrh, podobné modelky"], sc_cookie: ["Lišta cookies","Oznámení o cookies dole"], sc_footer: ["Patička","Patička a právní text"], sc_goal: ["Cíl (goal)","Ukazatel postupu cíle"], sc_hdr: ["Hlavička webu","Hlavička s logem, hledáním a tlačítky přihlášení"], sc_king: ["Král tipů","Pruh krále tipů"], sc_promo: ["Reklama a bannery","Losování tokenů, tlačítko „Připojit se“ na přehrávači"], sc_sub: ["Lišta modelky","Avatar, sledování a menu nad přehrávačem"], sc_topic: ["Téma místnosti","Text tématu nad přehrávačem i v něm"], sc_widgets: ["Widgety přehrávače","Ankety a reakce přes video"], sc_wm: ["Vodoznak","Logo přes video"], seotitle: ["Reklamní nadpis","SEO nadpis nad seznamem"], tabs: ["Záložky a řazení","Dívky/páry, filtry, ikona řazení"], tipking: ["Král tipů","Královský pruh a okno tipů"], tips: ["Nápovědy v chatu","Bubliny a oznámení"], wm: ["Vodoznak (PC)","Logo přes video"], }, RO: { actions: ["Butoanele „Privat / Tip”","Butoane albastre (pe PC verzi) sub bara roșie"], age18: ["Avertisment 18+","Ecranul de intrare „Doar 18+”"], balance: ["Sold și meniu (PC)","Soldul și navigarea pe computer"], below: ["Bara de sub player (PC)","Rândul de butoane de sub video"], cb_actions: ["Butonul de tip","Buton și formular pentru trimiterea tokenilor"], cb_alerts: ["Notificări","Avertisment de limbă și note închise"], cb_footer: ["Subsolul paginii","Footer și rețele sociale"], cb_hdr: ["Antetul site-ului","Antet cu logo și meniu"], cb_info: ["Numele camerei","Nume și contor de spectatori în antet"], cb_sort: ["Sortare","Panou de sortare în versiunea mobilă"], cb_status: ["Starea camerei","Banda offline și privat cu indicii"], cb_tags: ["Banda de taguri","Taguri populare deasupra listei"], cb_tips: ["Tipuri în chat","Mesaje despre tokeni și tipuri"], chat_boost: ["Boost-uri în chat","Carduri „a boostat modelul N secunde”"], chat_tips: ["Tipuri în chat","Mesaje „a dat tokeni”, meniul de tip"], chat_topic: ["Tema camerei","Bloc lung „Temă: …” la începutul chatului"], chathdr: ["Bara „Chat cu …”","Banda „Chat gratuit cu …” și regele tipurilor"], ftr: ["Subsolul paginii","Footer, rețele sociale, text SEO"], games: ["Butonul „Jocuri”","Pictograma de gamepad din bara roșie"], hdr_d: ["Antetul site-ului (PC)","Bara de sus pe computer"], hdr_m: ["Antetul site-ului","Logo, meniu, butoane de autentificare"], joinpop: ["Fereastra de înregistrare","Formularul pop-up „Creează cont”"], lastview: ["Mesajul „Continuă vizionarea”","Buton pe previzualizarea ultimului model"], modelchat: ["Chatul pop-up al modelului","Fereastră roz cu salut automat jos"], popups: ["Ferestre pop-up","Alegerea chatului, grup, voyeur"], promo: ["Reclame și bannere","Bannere, promo, bara „Alătură-te” de pe player, aplicația"], sc_about: ["Blocuri sub player","Despre mine, program, modele similare"], sc_cookie: ["Aviz cookie","Notificare cookie în partea de jos"], sc_footer: ["Subsolul paginii","Footer și text legal"], sc_goal: ["Obiectiv (goal)","Bara de progres a obiectivului"], sc_hdr: ["Antetul site-ului","Antet cu logo, căutare și butoane de autentificare"], sc_king: ["Regele tipurilor","Banda regelui tipurilor"], sc_promo: ["Reclame și bannere","Tombole de tokeni, butonul „Alătură-te” de pe player"], sc_sub: ["Bara modelului","Avatar, urmărire și meniu deasupra playerului"], sc_topic: ["Tema camerei","Textul temei deasupra și în player"], sc_widgets: ["Widgeturi pe player","Sondaje și reacții peste video"], sc_wm: ["Filigran","Logo peste video"], seotitle: ["Titlu publicitar","Titlu SEO deasupra listei"], tabs: ["Taburi și sortare","Fete/cupluri, filtre, pictogramă de sortare"], tipking: ["Regele tipurilor","Banda regelui și fereastra de tip"], tips: ["Sfaturi în chat","Tooltip-uri și notificări"], wm: ["Filigran (PC)","Logo peste video"], }, HU: { actions: ["„Privát / Borravaló” gombok","Kék (PC-n zöld) gombok a piros sáv alatt"], age18: ["18+ figyelmeztetés","„Csak 18+” belépő képernyő"], balance: ["Egyenleg és menü (PC)","Egyenleg és navigáció számítógépen"], below: ["Sáv a lejátszó alatt (PC)","Gombsor a videó alatt"], cb_actions: ["Borravaló gomb","Gomb és űrlap tokenek küldéséhez"], cb_alerts: ["Értesítések","Nyelvi figyelmeztetés és bezárható jegyzetek"], cb_footer: ["Oldal alja","Lábléc és közösségi média"], cb_hdr: ["Oldal fejléce","Fejléc logóval és menüvel"], cb_info: ["Szoba neve","Név és nézőszámláló a fejlécben"], cb_sort: ["Rendezés","Rendezési panel mobil nézetben"], cb_status: ["Szoba állapota","Offline és privát sáv tippekkel"], cb_tags: ["Címkesáv","Népszerű címkék a lista felett"], cb_tips: ["Borravaló a csevegésben","Token és borravaló üzenetek"], chat_boost: ["Boostok a csevegésben","„N másodpercre boostolta a modellt” kártyák"], chat_tips: ["Borravaló a csevegésben","„tokent adott” üzenetek, borravaló menü"], chat_topic: ["Szoba témája","Hosszú „Téma: …” blokk a csevegés elején"], chathdr: ["„Csevegés vele: …” sáv","„Ingyenes csevegés vele: …” csík és borravalókirály"], ftr: ["Oldal alja","Lábléc, közösségi média, SEO-szöveg"], games: ["„Játékok” gomb","Gamepad ikon a piros sávban"], hdr_d: ["Oldal fejléce (PC)","Felső sáv számítógépen"], hdr_m: ["Oldal fejléce","Logó, menü, belépés gombok"], joinpop: ["Regisztrációs ablak","„Fiók létrehozása” felugró űrlap"], lastview: ["„Folytatás” jelzés","Gomb az utoljára nézett modell előnézetén"], modelchat: ["A modell felugró csevegése","Rózsaszín ablak automatikus köszöntéssel alul"], popups: ["Felugró ablakok","Csevegés választás, csoport, voyeur"], promo: ["Reklámok és bannerek","Bannerek, promók, „Csatlakozz” sáv a lejátszón, app"], sc_about: ["Blokkok a lejátszó alatt","Rólam, menetrend, hasonló modellek"], sc_cookie: ["Süti értesítés","Süti figyelmeztetés alul"], sc_footer: ["Oldal alja","Lábléc és jogi szöveg"], sc_goal: ["Cél (goal)","A cél haladási sávja"], sc_hdr: ["Oldal fejléce","Fejléc logóval, kereséssel és belépés gombokkal"], sc_king: ["Borravalókirály","A borravalókirály sávja"], sc_promo: ["Reklámok és bannerek","Token-sorsolások, „Csatlakozz” gomb a lejátszón"], sc_sub: ["Modell sáv","Avatar, követés és menü a lejátszó felett"], sc_topic: ["Szoba témája","A téma szövege a lejátszó felett és benne"], sc_widgets: ["Lejátszó widgetek","Szavazások és reakciók a videó felett"], sc_wm: ["Vízjel","Logó a videó felett"], seotitle: ["Reklámcím","SEO-cím a lista felett"], tabs: ["Fülek és rendezés","Lányok/párok, szűrők, rendezés ikon"], tipking: ["Borravalókirály","Király csík és borravaló ablak"], tips: ["Csevegési tippek","Buborékok és értesítések"], wm: ["Vízjel (PC)","Logó a videó felett"], }, BG: { actions: ["Бутони „Приват / Бакшиш“","Сини (на PC зелени) бутони под червената лента"], age18: ["Предупреждение 18+","Входен екран „Само 18+“"], balance: ["Баланс и меню (PC)","Баланс и навигация на компютър"], below: ["Лента под плеъра (PC)","Ред с бутони под видеото"], cb_actions: ["Бутон за бакшиш","Бутон и форма за изпращане на токени"], cb_alerts: ["Известия","Предупреждение за език и затваряеми бележки"], cb_footer: ["Долна част на страницата","Футър и социални мрежи"], cb_hdr: ["Хедър на сайта","Хедър с лого и меню"], cb_info: ["Име на стаята","Име и брояч на зрители в хедъра"], cb_sort: ["Сортиране","Панел за сортиране в мобилната версия"], cb_status: ["Статус на стаята","Лента за офлайн и приват с подсказки"], cb_tags: ["Лента с тагове","Популярни тагове над списъка"], cb_tips: ["Бакшиши в чата","Съобщения за токени и бакшиши"], chat_boost: ["Бустове в чата","Карти „бустна модела за N секунди“"], chat_tips: ["Бакшиши в чата","Съобщения „даде токени“, меню за бакшиш"], chat_topic: ["Тема на стаята","Дълъг блок „Тема: …“ в началото на чата"], chathdr: ["Лента „Чат с …“","Ивица „Безплатен чат с …“ и цар на бакшишите"], ftr: ["Долна част на страницата","Футър, социални мрежи, SEO текст"], games: ["Бутон „Игри“","Иконка на геймпад в червената лента"], hdr_d: ["Хедър на сайта (PC)","Горна лента на компютър"], hdr_m: ["Хедър на сайта","Лого, меню, бутони за вход"], joinpop: ["Прозорец за регистрация","Изскачаща форма „Създай акаунт“"], lastview: ["Плашка „Продължи гледането“","Бутон на превюто на последния модел"], modelchat: ["Изскачащ чат на модела","Розов прозорец с автоматичен поздрав долу"], popups: ["Изскачащи прозорци","Избор на чат, група, воаяор"], promo: ["Реклами и банери","Банери, промо, лента „Присъедини се“ на плеъра, приложение"], sc_about: ["Блокове под плеъра","За мен, разписание, подобни модели"], sc_cookie: ["Известие за бисквитки","Известие за бисквитки долу"], sc_footer: ["Долна част на страницата","Футър и правен текст"], sc_goal: ["Цел (goal)","Лента за напредъка на целта"], sc_hdr: ["Хедър на сайта","Хедър с лого, търсене и бутони за вход"], sc_king: ["Цар на бакшишите","Ивица на царя на бакшишите"], sc_promo: ["Реклами и банери","Томболи с токени, бутон „Присъедини се“ на плеъра"], sc_sub: ["Лента на модела","Аватар, следване и меню над плеъра"], sc_topic: ["Тема на стаята","Текст на темата над и в плеъра"], sc_widgets: ["Приспособления на плеъра","Анкети и реакции върху видеото"], sc_wm: ["Воден знак","Лого върху видеото"], seotitle: ["Рекламно заглавие","SEO заглавие над списъка"], tabs: ["Раздели и сортиране","Момичета/двойки, филтри, иконка за сортиране"], tipking: ["Цар на бакшишите","Царска ивица и прозорец за бакшиш"], tips: ["Съвети в чата","Подсказки и известия"], wm: ["Воден знак (PC)","Лого върху видеото"], }, EL: { actions: ["Κουμπιά «Ιδιωτικό / Φιλοδώρημα»","Μπλε (στον υπολογιστή πράσινα) κουμπιά κάτω από την κόκκινη γραμμή"], age18: ["Ειδοποίηση 18+","Αρχική οθόνη «Μόνο 18+»"], balance: ["Υπόλοιπο και μενού (PC)","Υπόλοιπο και πλοήγηση στον υπολογιστή"], below: ["Γραμμή κάτω από τον player (PC)","Σειρά κουμπιών κάτω από το βίντεο"], cb_actions: ["Κουμπί tip","Κουμπί και φόρμα αποστολής tokens"], cb_alerts: ["Ειδοποιήσεις","Προειδοποίηση γλώσσας και σημειώσεις που κλείνουν"], cb_footer: ["Υποσέλιδο","Footer και social"], cb_hdr: ["Κεφαλίδα ιστότοπου","Κεφαλίδα με λογότυπο και μενού"], cb_info: ["Όνομα δωματίου","Όνομα και μετρητής θεατών στην κεφαλίδα"], cb_sort: ["Ταξινόμηση","Πάνελ ταξινόμησης στην έκδοση για κινητά"], cb_status: ["Κατάσταση δωματίου","Λωρίδα offline και private με υποδείξεις"], cb_tags: ["Λωρίδα ετικετών","Δημοφιλείς ετικέτες πάνω από τη λίστα"], cb_tips: ["Φιλοδωρήματα στο chat","Μηνύματα για tokens και tips"], chat_boost: ["Boosts στο chat","Κάρτες «ενίσχυσε το μοντέλο για N δευτερόλεπτα»"], chat_tips: ["Φιλοδωρήματα στο chat","Μηνύματα «έδωσε tokens», μενού tip"], chat_topic: ["Θέμα δωματίου","Μεγάλο μπλοκ «Θέμα: …» στην αρχή του chat"], chathdr: ["Γραμμή «Chat με …»","Λωρίδα «Δωρεάν chat με …» και βασιλιάς φιλοδωρημάτων"], ftr: ["Υποσέλιδο","Footer, social, κείμενο SEO"], games: ["Κουμπί «Παιχνίδια»","Εικονίδιο gamepad στην κόκκινη γραμμή"], hdr_d: ["Κεφαλίδα ιστότοπου (PC)","Επάνω γραμμή στον υπολογιστή"], hdr_m: ["Κεφαλίδα ιστότοπου","Λογότυπο, μενού, κουμπιά σύνδεσης"], joinpop: ["Παράθυρο εγγραφής","Αναδυόμενη φόρμα «Δημιουργία λογαριασμού»"], lastview: ["Ειδοποίηση «Συνέχεια παρακολούθησης»","Κουμπί στην προεπισκόπηση του τελευταίου μοντέλου"], modelchat: ["Αναδυόμενο chat μοντέλου","Ροζ παράθυρο με αυτόματο χαιρετισμό κάτω"], popups: ["Αναδυόμενα παράθυρα","Επιλογή chat, ομάδα, voyeur"], promo: ["Διαφημίσεις και banners","Banners, promo, γραμμή «Γίνε μέλος» στον player, εφαρμογή"], sc_about: ["Μπλοκ κάτω από τον player","Σχετικά με μένα, πρόγραμμα, παρόμοια μοντέλα"], sc_cookie: ["Ειδοποίηση cookies","Ειδοποίηση για cookies στο κάτω μέρος"], sc_footer: ["Υποσέλιδο","Footer και νομικό κείμενο"], sc_goal: ["Στόχος (goal)","Γραμμή προόδου του στόχου"], sc_hdr: ["Κεφαλίδα ιστότοπου","Κεφαλίδα με λογότυπο, αναζήτηση και κουμπιά σύνδεσης"], sc_king: ["Βασιλιάς φιλοδωρημάτων","Λωρίδα του βασιλιά tip"], sc_promo: ["Διαφημίσεις και banners","Κληρώσεις tokens, κουμπί «Γίνε μέλος» στον player"], sc_sub: ["Γραμμή μοντέλου","Avatar, ακολούθηση και μενού πάνω από τον player"], sc_topic: ["Θέμα δωματίου","Κείμενο θέματος πάνω από και μέσα στον player"], sc_widgets: ["Widgets του player","Δημοσκοπήσεις και αντιδράσεις πάνω από το βίντεο"], sc_wm: ["Υδατογράφημα","Λογότυπο πάνω από το βίντεο"], seotitle: ["Διαφημιστικός τίτλος","Τίτλος SEO πάνω από τη λίστα"], tabs: ["Καρτέλες και ταξινόμηση","Κορίτσια/ζευγάρια, φίλτρα, εικονίδιο ταξινόμησης"], tipking: ["Βασιλιάς φιλοδωρημάτων","Λωρίδα βασιλιά και παράθυρο φιλοδωρημάτων"], tips: ["Συμβουλές chat","Tooltips και ειδοποιήσεις"], wm: ["Υδατογράφημα (PC)","Λογότυπο πάνω από το βίντεο"], }, JA: { actions: ["「プライベート / チップ」ボタン","赤いバー下の青い(PCでは緑の)ボタン"], age18: ["18歳以上の確認","「18歳以上のみ」入口画面"], balance: ["残高とメニュー(PC)","パソコン表示の残高とナビゲーション"], below: ["プレーヤー下のバー(PC)","動画下のボタン列"], cb_actions: ["チップボタン","トークン送信のボタンとフォーム"], cb_alerts: ["通知","言語の警告と閉じられるメモ"], cb_footer: ["ページ下部","フッターとSNS"], cb_hdr: ["サイトのヘッダー","ロゴとメニューのあるヘッダー"], cb_info: ["ルーム名","ヘッダーの名前と視聴者数"], cb_sort: ["並べ替え","モバイル版の並べ替えパネル"], cb_status: ["ルームの状態","オフラインとプライベートの案内帯"], cb_tags: ["タグの帯","リスト上の人気タグ"], cb_tips: ["チャットのチップ","トークンとチップのメッセージ"], chat_boost: ["チャットのブースト","「モデルをN秒ブースト」カード"], chat_tips: ["チャットのチップ","「トークンを贈りました」メッセージ、チップメニュー"], chat_topic: ["ルームのトピック","チャット冒頭の長い「トピック: …」ブロック"], chathdr: ["「〜とチャット」バー","「〜と無料チャット」帯とチップキング"], ftr: ["ページ下部","フッター、SNS、SEOテキスト"], games: ["「ゲーム」ボタン","赤いバーのゲームパッドアイコン"], hdr_d: ["サイトのヘッダー(PC)","パソコン表示の上部バー"], hdr_m: ["サイトのヘッダー","ロゴ、メニュー、ログインボタン"], joinpop: ["登録ウィンドウ","「アカウント作成」ポップアップ"], lastview: ["「続きを見る」表示","最後に見たモデルのプレビュー上のボタン"], modelchat: ["モデルのポップアップチャット","下部に自動あいさつが出るピンクのウィンドウ"], popups: ["ポップアップウィンドウ","チャット選択、グループ、のぞき見"], promo: ["広告とバナー","バナー、プロモ、プレーヤー上の「参加」バー、アプリ"], sc_about: ["プレーヤー下のブロック","自己紹介、スケジュール、似たモデル"], sc_cookie: ["Cookie通知","下部のCookie通知"], sc_footer: ["ページ下部","フッターと法的テキスト"], sc_goal: ["目標(ゴール)","目標の進行バー"], sc_hdr: ["サイトのヘッダー","ロゴ、検索、ログインボタンのあるヘッダー"], sc_king: ["チップキング","チップキングの帯"], sc_promo: ["広告とバナー","トークン抽選、プレーヤー上の「参加」ボタン"], sc_sub: ["モデルバー","プレーヤー上のアバター、フォロー、メニュー"], sc_topic: ["ルームのトピック","プレーヤー上と内部のトピック文"], sc_widgets: ["プレーヤーのウィジェット","動画上のアンケートとリアクション"], sc_wm: ["透かし","動画上のロゴ"], seotitle: ["宣伝見出し","リスト上のSEO見出し"], tabs: ["タブと並べ替え","女の子/カップル、フィルター、並べ替えアイコン"], tipking: ["チップキング","キングの帯とチップウィンドウ"], tips: ["チャットのヒント","ツールチップと通知"], wm: ["透かし(PC)","動画上のロゴ"], }, ZH: { actions: ["“私密 / 打赏”按钮","红色栏下方的蓝色(电脑端为绿色)按钮"], age18: ["18+ 提示","“仅限 18 岁以上”入口页"], balance: ["余额和菜单(电脑)","电脑端的余额和导航"], below: ["播放器下方栏(电脑)","视频下方的按钮行"], cb_actions: ["打赏按钮","发送代币的按钮和表单"], cb_alerts: ["通知","语言提示和可关闭的说明"], cb_footer: ["页面底部","页脚和社交媒体"], cb_hdr: ["网站顶部","带有 Logo 和菜单的页头"], cb_info: ["房间名称","页头中的名称和观众数"], cb_sort: ["排序","移动版的排序面板"], cb_status: ["房间状态","离线和私密提示条"], cb_tags: ["标签条","列表上方的热门标签"], cb_tips: ["聊天打赏","代币和打赏消息"], chat_boost: ["聊天加成","“为主播加成 N 秒”卡片"], chat_tips: ["聊天打赏","“赠送了代币”消息、打赏菜单"], chat_topic: ["房间话题","聊天开头很长的“话题:…”块"], chathdr: ["“与…聊天”栏","“与…免费聊天”条和打赏之王"], ftr: ["页面底部","页脚、社交媒体、SEO 文字"], games: ["“游戏”按钮","红色栏中的手柄图标"], hdr_d: ["网站顶部(电脑)","电脑端的顶部栏"], hdr_m: ["网站顶部","Logo、菜单、登录按钮"], joinpop: ["注册窗口","“创建账号”弹窗"], lastview: ["“继续观看”提示","最近观看主播预览上的按钮"], modelchat: ["主播弹出聊天","底部带自动问候的粉色窗口"], popups: ["弹出窗口","聊天选择、群组、偷看"], promo: ["广告和横幅","横幅、推广、播放器上的“加入”条、应用"], sc_about: ["播放器下方区块","关于我、日程、相似主播"], sc_cookie: ["Cookie 提示","底部的 Cookie 通知"], sc_footer: ["页面底部","页脚和法律文字"], sc_goal: ["目标(goal)","目标进度条"], sc_hdr: ["网站顶部","带有 Logo、搜索和登录按钮的页头"], sc_king: ["打赏之王","打赏之王条"], sc_promo: ["广告和横幅","代币抽奖、播放器上的“加入”按钮"], sc_sub: ["主播栏","播放器上方的头像、关注和菜单"], sc_topic: ["房间话题","播放器上方和内部的话题文字"], sc_widgets: ["播放器小组件","视频上的投票和互动"], sc_wm: ["水印","视频上的 Logo"], seotitle: ["广告标题","列表上方的 SEO 标题"], tabs: ["标签页和排序","女生/情侣、筛选、排序图标"], tipking: ["打赏之王","国王条和打赏窗口"], tips: ["聊天提示","提示气泡和通知"], wm: ["水印(电脑)","视频上的 Logo"], }, KO: { actions: ["“프라이빗 / 팁” 버튼","빨간 바 아래의 파란색(PC에서는 녹색) 버튼"], age18: ["18+ 안내","“18세 이상만” 입장 화면"], balance: ["잔액 및 메뉴(PC)","컴퓨터 화면의 잔액과 내비게이션"], below: ["플레이어 아래 바(PC)","영상 아래의 버튼 줄"], cb_actions: ["팁 버튼","토큰 전송 버튼과 양식"], cb_alerts: ["알림","언어 경고와 닫을 수 있는 메모"], cb_footer: ["페이지 하단","푸터와 SNS"], cb_hdr: ["사이트 상단","로고와 메뉴가 있는 헤더"], cb_info: ["방 이름","헤더의 이름과 시청자 수"], cb_sort: ["정렬","모바일 버전의 정렬 패널"], cb_status: ["방 상태","오프라인과 프라이빗 안내 띠"], cb_tags: ["태그 띠","목록 위의 인기 태그"], cb_tips: ["채팅 팁","토큰과 팁 메시지"], chat_boost: ["채팅 부스트","“모델을 N초 부스트했습니다” 카드"], chat_tips: ["채팅 팁","“토큰을 선물했습니다” 메시지, 팁 메뉴"], chat_topic: ["방 주제","채팅 시작 부분의 긴 “주제: …” 블록"], chathdr: ["“…와 채팅” 바","“…와 무료 채팅” 띠와 팁 킹"], ftr: ["페이지 하단","푸터, SNS, SEO 텍스트"], games: ["“게임” 버튼","빨간 바의 게임패드 아이콘"], hdr_d: ["사이트 헤더(PC)","컴퓨터 화면의 상단 바"], hdr_m: ["사이트 헤더","로고, 메뉴, 로그인 버튼"], joinpop: ["가입 창","“계정 만들기” 팝업"], lastview: ["“이어보기” 표시","마지막으로 본 모델 미리보기의 버튼"], modelchat: ["모델 팝업 채팅","하단에 자동 인사가 있는 분홍색 창"], popups: ["팝업 창","채팅 선택, 그룹, 훔쳐보기"], promo: ["광고 및 배너","배너, 프로모션, 플레이어의 “가입” 바, 앱"], sc_about: ["플레이어 아래 블록","소개, 일정, 비슷한 모델"], sc_cookie: ["쿠키 안내","하단의 쿠키 알림"], sc_footer: ["페이지 하단","푸터와 법적 문구"], sc_goal: ["목표(골)","목표 진행 바"], sc_hdr: ["사이트 상단","로고, 검색, 로그인 버튼이 있는 헤더"], sc_king: ["팁 킹","팁 킹 띠"], sc_promo: ["광고 및 배너","토큰 추첨, 플레이어의 “참여” 버튼"], sc_sub: ["모델 바","플레이어 위의 아바타, 팔로우, 메뉴"], sc_topic: ["방 주제","플레이어 위와 안의 주제 문구"], sc_widgets: ["플레이어 위젯","영상 위의 투표와 반응"], sc_wm: ["워터마크","영상 위의 로고"], seotitle: ["광고성 제목","목록 위의 SEO 제목"], tabs: ["탭과 정렬","여성/커플, 필터, 정렬 아이콘"], tipking: ["팁 킹","킹 띠와 팁 창"], tips: ["채팅 안내","툴팁과 알림"], wm: ["워터마크(PC)","영상 위의 로고"], }, HE: { actions: ["כפתורי “פרטי / טיפ”","כפתורים כחולים (במחשב ירוקים) מתחת לפס האדום"], age18: ["הודעת 18+","מסך כניסה “18 ומעלה בלבד”"], balance: ["יתרה ותפריט (מחשב)","יתרה וניווט במחשב"], below: ["פס מתחת לנגן (מחשב)","שורת כפתורים מתחת לוידאו"], cb_actions: ["כפתור טיפ","כפתור וטופס לשליחת טוקנים"], cb_alerts: ["התראות","אזהרת שפה והערות שניתן לסגור"], cb_footer: ["תחתית העמוד","פוטר ורשתות חברתיות"], cb_hdr: ["כותרת האתר","כותרת עם לוגו ותפריט"], cb_info: ["שם החדר","שם ומונה צופים בכותרת"], cb_sort: ["מיון","לוח מיון בגרסה לנייד"], cb_status: ["סטטוס החדר","רצועת לא מחובר ופרטי עם רמזים"], cb_tags: ["רצועת תגיות","תגיות פופולריות מעל הרשימה"], cb_tips: ["טיפים בצ׳אט","הודעות על טוקנים וטיפים"], chat_boost: ["בוסטים בצ׳אט","כרטיסים “הגביר את המודל ל-N שניות”"], chat_tips: ["טיפים בצ׳אט","הודעות “שלח טוקנים”, תפריט טיפים"], chat_topic: ["נושא החדר","בלוק ארוך “נושא: …” בתחילת הצ׳אט"], chathdr: ["פס “צ׳אט עם …”","רצועת “צ׳אט חינם עם …” ומלך הטיפים"], ftr: ["תחתית העמוד","פוטר, רשתות חברתיות, טקסט SEO"], games: ["כפתור “משחקים”","סמל שלט משחק בפס האדום"], hdr_d: ["כותרת האתר (מחשב)","הסרגל העליון במחשב"], hdr_m: ["כותרת האתר","לוגו, תפריט, כפתורי כניסה"], joinpop: ["חלון הרשמה","חלון קופץ “יצירת חשבון”"], lastview: ["הודעת “המשך צפייה”","כפתור על התצוגה המקדימה של המודל האחרון"], modelchat: ["צ׳אט קופץ של המודל","חלון ורוד עם ברכה אוטומטית בתחתית"], popups: ["חלונות קופצים","בחירת צ׳אט, קבוצה, מציצן"], promo: ["פרסומות ובאנרים","באנרים, מבצעים, פס “הצטרף” בנגן, אפליקציה"], sc_about: ["בלוקים מתחת לנגן","אודותיי, לוח זמנים, מודלים דומים"], sc_cookie: ["הודעת קוקיז","הודעת קוקיז בתחתית"], sc_footer: ["תחתית העמוד","פוטר וטקסט משפטי"], sc_goal: ["יעד (goal)","סרגל התקדמות היעד"], sc_hdr: ["כותרת האתר","כותרת עם לוגו, חיפוש וכפתורי כניסה"], sc_king: ["מלך הטיפים","רצועת מלך הטיפים"], sc_promo: ["פרסומות ובאנרים","הגרלות טוקנים, כפתור “הצטרף” בנגן"], sc_sub: ["סרגל המודל","אווטאר, מעקב ותפריט מעל הנגן"], sc_topic: ["נושא החדר","טקסט הנושא מעל הנגן ובתוכו"], sc_widgets: ["ווידג׳טים בנגן","סקרים ותגובות מעל הוידאו"], sc_wm: ["סימן מים","לוגו מעל הוידאו"], seotitle: ["כותרת פרסומית","כותרת SEO מעל הרשימה"], tabs: ["לשוניות ומיון","בנות/זוגות, סינון, סמל מיון"], tipking: ["מלך הטיפים","רצועת המלך וחלון הטיפים"], tips: ["רמזים בצ׳אט","טיפים צפים והתראות"], wm: ["סימן מים (מחשב)","לוגו מעל הוידאו"], }, AR: { actions: ["أزرار «خاص / بقشيش»","أزرار زرقاء (خضراء على الكمبيوتر) تحت الشريط الأحمر"], age18: ["تنبيه +18","شاشة الدخول «للكبار فقط 18+»"], balance: ["الرصيد والقائمة (الكمبيوتر)","الرصيد والتنقل على الكمبيوتر"], below: ["الشريط تحت المشغّل (الكمبيوتر)","صف الأزرار تحت الفيديو"], cb_actions: ["زر البقشيش","زر ونموذج إرسال الرموز"], cb_alerts: ["الإشعارات","تنبيه اللغة وملاحظات قابلة للإغلاق"], cb_footer: ["أسفل الصفحة","التذييل والشبكات الاجتماعية"], cb_hdr: ["رأس الموقع","رأس مع الشعار والقائمة"], cb_info: ["اسم الغرفة","الاسم وعدّاد المشاهدين في الرأس"], cb_sort: ["الفرز","لوحة الفرز في نسخة الجوال"], cb_status: ["حالة الغرفة","شريط غير متصل وخاص مع تلميحات"], cb_tags: ["شريط الوسوم","وسوم شائعة فوق القائمة"], cb_tips: ["البقشيش في الدردشة","رسائل الرموز والبقشيش"], chat_boost: ["التعزيزات في الدردشة","بطاقات «عزّز الموديل لمدة N ثانية»"], chat_tips: ["البقشيش في الدردشة","رسائل «أهدى رموزًا»، قائمة البقشيش"], chat_topic: ["موضوع الغرفة","كتلة طويلة «الموضوع: …» في بداية الدردشة"], chathdr: ["شريط «دردشة مع …»","شريط «دردشة مجانية مع …» وملك البقشيش"], ftr: ["أسفل الصفحة","التذييل، الشبكات الاجتماعية، نص SEO"], games: ["زر «الألعاب»","أيقونة يد التحكم في الشريط الأحمر"], hdr_d: ["رأس الموقع (الكمبيوتر)","الشريط العلوي على الكمبيوتر"], hdr_m: ["رأس الموقع","الشعار والقائمة وأزرار الدخول"], joinpop: ["نافذة التسجيل","نافذة منبثقة «إنشاء حساب»"], lastview: ["تنبيه «متابعة المشاهدة»","زر على معاينة آخر موديل"], modelchat: ["دردشة الموديل المنبثقة","نافذة وردية بتحية تلقائية في الأسفل"], popups: ["النوافذ المنبثقة","اختيار الدردشة، المجموعة، المتلصص"], promo: ["الإعلانات واللافتات","لافتات، عروض، شريط «انضم» على المشغّل، التطبيق"], sc_about: ["كتل أسفل المشغّل","نبذة عني والجدول وموديلات مشابهة"], sc_cookie: ["إشعار الكوكيز","إشعار الكوكيز في الأسفل"], sc_footer: ["أسفل الصفحة","التذييل والنص القانوني"], sc_goal: ["الهدف (goal)","شريط تقدّم الهدف"], sc_hdr: ["رأس الموقع","رأس مع الشعار والبحث وأزرار الدخول"], sc_king: ["ملك البقشيش","شريط ملك البقشيش"], sc_promo: ["الإعلانات واللافتات","سحوبات الرموز وزر «انضم» على المشغّل"], sc_sub: ["شريط الموديل","الصورة الرمزية والمتابعة والقائمة فوق المشغّل"], sc_topic: ["موضوع الغرفة","نص الموضوع فوق المشغّل وداخله"], sc_widgets: ["أدوات المشغّل","استطلاعات وتفاعلات فوق الفيديو"], sc_wm: ["العلامة المائية","الشعار فوق الفيديو"], seotitle: ["عنوان إعلاني","عنوان SEO فوق القائمة"], tabs: ["التبويبات والفرز","فتيات/أزواج، فلاتر، أيقونة الفرز"], tipking: ["ملك البقشيش","شريط الملك ونافذة البقشيش"], tips: ["تلميحات الدردشة","تلميحات وإشعارات"], wm: ["العلامة المائية (الكمبيوتر)","الشعار فوق الفيديو"], }, HI: { actions: ["“प्राइवेट / टिप” बटन","लाल पट्टी के नीचे नीले (PC पर हरे) बटन"], age18: ["18+ चेतावनी","“केवल 18+” एंट्री स्क्रीन"], balance: ["बैलेंस और मेन्यू (PC)","कंप्यूटर पर बैलेंस और नेविगेशन"], below: ["प्लेयर के नीचे की पट्टी (PC)","वीडियो के नीचे बटनों की पंक्ति"], cb_actions: ["टिप बटन","टोकन भेजने का बटन और फ़ॉर्म"], cb_alerts: ["सूचनाएँ","भाषा की चेतावनी और बंद होने वाले नोट"], cb_footer: ["पेज का निचला हिस्सा","फ़ुटर और सोशल मीडिया"], cb_hdr: ["साइट का हेडर","लोगो और मेन्यू वाला हेडर"], cb_info: ["रूम का नाम","हेडर में नाम और दर्शकों की गिनती"], cb_sort: ["सॉर्टिंग","मोबाइल संस्करण में सॉर्ट पैनल"], cb_status: ["रूम की स्थिति","ऑफ़लाइन और प्राइवेट पट्टी, सुझावों के साथ"], cb_tags: ["टैग पट्टी","सूची के ऊपर लोकप्रिय टैग"], cb_tips: ["चैट में टिप","टोकन और टिप के संदेश"], chat_boost: ["चैट में बूस्ट","“मॉडल को N सेकंड बूस्ट किया” कार्ड"], chat_tips: ["चैट में टिप","“टोकन दिए” संदेश, टिप मेन्यू"], chat_topic: ["रूम का विषय","चैट की शुरुआत में लंबा “विषय: …” ब्लॉक"], chathdr: ["“… के साथ चैट” पट्टी","“… के साथ मुफ़्त चैट” पट्टी और टिप किंग"], ftr: ["पेज का निचला हिस्सा","फ़ुटर, सोशल मीडिया, SEO टेक्स्ट"], games: ["“गेम” बटन","लाल पट्टी में गेमपैड आइकन"], hdr_d: ["साइट का हेडर (PC)","कंप्यूटर पर ऊपर की पट्टी"], hdr_m: ["साइट का हेडर","लोगो, मेन्यू, लॉगिन बटन"], joinpop: ["रजिस्ट्रेशन विंडो","“खाता बनाएँ” पॉप-अप"], lastview: ["“देखना जारी रखें” सूचना","आख़िरी देखी मॉडल के प्रीव्यू पर बटन"], modelchat: ["मॉडल का पॉप-अप चैट","नीचे ऑटो-अभिवादन वाली गुलाबी विंडो"], popups: ["पॉप-अप विंडो","चैट चयन, ग्रुप, वॉयर"], promo: ["विज्ञापन और बैनर","बैनर, प्रोमो, प्लेयर पर “जुड़ें” पट्टी, ऐप"], sc_about: ["प्लेयर के नीचे के ब्लॉक","मेरे बारे में, शेड्यूल, मिलती-जुलती मॉडल"], sc_cookie: ["कुकी सूचना","नीचे कुकी की सूचना"], sc_footer: ["पेज का निचला हिस्सा","फ़ुटर और कानूनी टेक्स्ट"], sc_goal: ["लक्ष्य (गोल)","लक्ष्य की प्रगति पट्टी"], sc_hdr: ["साइट का हेडर","लोगो, खोज और लॉगिन बटन वाला हेडर"], sc_king: ["टिप किंग","टिप किंग की पट्टी"], sc_promo: ["विज्ञापन और बैनर","टोकन ड्रॉ, प्लेयर पर “जुड़ें” बटन"], sc_sub: ["मॉडल बार","प्लेयर के ऊपर अवतार, फ़ॉलो और मेन्यू"], sc_topic: ["रूम का विषय","प्लेयर के ऊपर और अंदर विषय का टेक्स्ट"], sc_widgets: ["प्लेयर विजेट","वीडियो के ऊपर पोल और रिएक्शन"], sc_wm: ["वॉटरमार्क","वीडियो के ऊपर लोगो"], seotitle: ["विज्ञापन शीर्षक","सूची के ऊपर SEO शीर्षक"], tabs: ["टैब और सॉर्टिंग","लड़कियाँ/जोड़े, फ़िल्टर, सॉर्ट आइकन"], tipking: ["टिप किंग","किंग पट्टी और टिप विंडो"], tips: ["चैट सुझाव","टूलटिप और सूचनाएँ"], wm: ["वॉटरमार्क (PC)","वीडियो के ऊपर लोगो"], }, ID: { actions: ["Tombol “Privat / Tip”","Tombol biru (hijau di PC) di bawah bilah merah"], age18: ["Peringatan 18+","Layar masuk “Khusus 18+”"], balance: ["Saldo dan menu (PC)","Saldo dan navigasi di komputer"], below: ["Bilah di bawah pemutar (PC)","Baris tombol di bawah video"], cb_actions: ["Tombol tip","Tombol dan formulir kirim token"], cb_alerts: ["Pemberitahuan","Peringatan bahasa dan catatan yang bisa ditutup"], cb_footer: ["Bagian bawah halaman","Footer dan media sosial"], cb_hdr: ["Header situs","Header dengan logo dan menu"], cb_info: ["Nama ruangan","Nama dan penghitung penonton di header"], cb_sort: ["Pengurutan","Panel urutkan di versi seluler"], cb_status: ["Status ruangan","Pita offline dan privat dengan petunjuk"], cb_tags: ["Pita tag","Tag populer di atas daftar"], cb_tips: ["Tip di chat","Pesan token dan tip"], chat_boost: ["Boost di chat","Kartu “mem-boost model selama N detik”"], chat_tips: ["Tip di chat","Pesan “memberi token”, menu tip"], chat_topic: ["Topik ruangan","Blok panjang “Topik: …” di awal chat"], chathdr: ["Bilah “Chat dengan …”","Pita “Chat gratis dengan …” dan raja tip"], ftr: ["Bagian bawah halaman","Footer, media sosial, teks SEO"], games: ["Tombol “Game”","Ikon gamepad di bilah merah"], hdr_d: ["Header situs (PC)","Bilah atas di komputer"], hdr_m: ["Header situs","Logo, menu, tombol masuk"], joinpop: ["Jendela pendaftaran","Pop-up “Buat akun”"], lastview: ["Pemberitahuan “Lanjutkan menonton”","Tombol di pratinjau model terakhir"], modelchat: ["Chat pop-up model","Jendela pink dengan sapaan otomatis di bawah"], popups: ["Jendela pop-up","Pilihan chat, grup, voyeur"], promo: ["Iklan dan banner","Banner, promo, bilah “Bergabung” di pemutar, aplikasi"], sc_about: ["Blok di bawah pemutar","Tentang saya, jadwal, model serupa"], sc_cookie: ["Pemberitahuan cookie","Pemberitahuan cookie di bagian bawah"], sc_footer: ["Bagian bawah halaman","Footer dan teks hukum"], sc_goal: ["Target (goal)","Bilah progres target"], sc_hdr: ["Header situs","Header dengan logo, pencarian, dan tombol masuk"], sc_king: ["Raja tip","Pita raja tip"], sc_promo: ["Iklan dan banner","Undian token, tombol “Bergabung” di pemutar"], sc_sub: ["Bilah model","Avatar, ikuti, dan menu di atas pemutar"], sc_topic: ["Topik ruangan","Teks topik di atas dan di dalam pemutar"], sc_widgets: ["Widget pemutar","Polling dan reaksi di atas video"], sc_wm: ["Watermark","Logo di atas video"], seotitle: ["Judul iklan","Judul SEO di atas daftar"], tabs: ["Tab dan pengurutan","Cewek/pasangan, filter, ikon urutkan"], tipking: ["Raja tip","Pita raja dan jendela tip"], tips: ["Petunjuk chat","Tooltip dan notifikasi"], wm: ["Watermark (PC)","Logo di atas video"], }, VI: { actions: ["Nút “Riêng tư / Tip”","Nút xanh dương (xanh lá trên PC) dưới thanh đỏ"], age18: ["Cảnh báo 18+","Màn hình vào “Chỉ dành cho 18+”"], balance: ["Số dư và menu (PC)","Số dư và điều hướng trên máy tính"], below: ["Thanh dưới trình phát (PC)","Hàng nút bên dưới video"], cb_actions: ["Nút tip","Nút và biểu mẫu gửi token"], cb_alerts: ["Thông báo","Cảnh báo ngôn ngữ và ghi chú có thể đóng"], cb_footer: ["Chân trang","Footer và mạng xã hội"], cb_hdr: ["Phần đầu trang","Header có logo và menu"], cb_info: ["Tên phòng","Tên và số người xem trong header"], cb_sort: ["Sắp xếp","Bảng sắp xếp trên bản di động"], cb_status: ["Trạng thái phòng","Dải offline và riêng tư kèm gợi ý"], cb_tags: ["Dải thẻ","Thẻ phổ biến phía trên danh sách"], cb_tips: ["Tip trong chat","Tin nhắn về token và tip"], chat_boost: ["Boost trong chat","Thẻ “đã boost model N giây”"], chat_tips: ["Tip trong chat","Tin nhắn “đã tặng token”, menu tip"], chat_topic: ["Chủ đề phòng","Khối dài “Chủ đề: …” ở đầu chat"], chathdr: ["Thanh “Chat với …”","Dải “Chat miễn phí với …” và vua tip"], ftr: ["Chân trang","Footer, mạng xã hội, văn bản SEO"], games: ["Nút “Trò chơi”","Biểu tượng tay cầm ở thanh đỏ"], hdr_d: ["Phần đầu trang (PC)","Thanh trên cùng trên máy tính"], hdr_m: ["Phần đầu trang","Logo, menu, nút đăng nhập"], joinpop: ["Cửa sổ đăng ký","Cửa sổ bật lên “Tạo tài khoản”"], lastview: ["Thông báo “Xem tiếp”","Nút trên bản xem trước của model xem gần nhất"], modelchat: ["Chat bật lên của model","Cửa sổ màu hồng có lời chào tự động ở dưới"], popups: ["Cửa sổ bật lên","Chọn chat, nhóm, voyeur"], promo: ["Quảng cáo và banner","Banner, khuyến mãi, thanh “Tham gia” trên trình phát, ứng dụng"], sc_about: ["Khối dưới trình phát","Giới thiệu, lịch, model tương tự"], sc_cookie: ["Thông báo cookie","Thông báo cookie ở phía dưới"], sc_footer: ["Chân trang","Footer và văn bản pháp lý"], sc_goal: ["Mục tiêu (goal)","Thanh tiến độ mục tiêu"], sc_hdr: ["Phần đầu trang","Header có logo, tìm kiếm và nút đăng nhập"], sc_king: ["Vua tip","Dải vua tip"], sc_promo: ["Quảng cáo và banner","Rút thăm token, nút “Tham gia” trên trình phát"], sc_sub: ["Thanh model","Avatar, theo dõi và menu phía trên trình phát"], sc_topic: ["Chủ đề phòng","Văn bản chủ đề phía trên và trong trình phát"], sc_widgets: ["Tiện ích trên trình phát","Bình chọn và phản ứng trên video"], sc_wm: ["Hình mờ","Logo trên video"], seotitle: ["Tiêu đề quảng cáo","Tiêu đề SEO phía trên danh sách"], tabs: ["Tab và sắp xếp","Các cô gái/cặp đôi, bộ lọc, biểu tượng sắp xếp"], tipking: ["Vua tip","Dải vua và cửa sổ tip"], tips: ["Gợi ý trong chat","Tooltip và thông báo"], wm: ["Hình mờ (PC)","Logo phía trên video"], }, TH: { actions: ["ปุ่ม “ส่วนตัว / ทิป”","ปุ่มสีน้ำเงิน (บน PC เป็นสีเขียว) ใต้แถบสีแดง"], age18: ["คำเตือน 18+","หน้าจอเข้าสู่ระบบ “เฉพาะ 18+”"], balance: ["ยอดเงินและเมนู (PC)","ยอดเงินและการนำทางบนคอมพิวเตอร์"], below: ["แถบใต้เครื่องเล่น (PC)","แถวปุ่มใต้วิดีโอ"], cb_actions: ["ปุ่มทิป","ปุ่มและฟอร์มส่งโทเคน"], cb_alerts: ["การแจ้งเตือน","คำเตือนเรื่องภาษาและโน้ตที่ปิดได้"], cb_footer: ["ท้ายหน้า","ส่วนท้ายและโซเชียล"], cb_hdr: ["ส่วนหัวเว็บไซต์","ส่วนหัวที่มีโลโก้และเมนู"], cb_info: ["ชื่อห้อง","ชื่อและตัวนับผู้ชมในส่วนหัว"], cb_sort: ["การเรียง","แผงเรียงในเวอร์ชันมือถือ"], cb_status: ["สถานะห้อง","แถบออฟไลน์และส่วนตัวพร้อมคำแนะนำ"], cb_tags: ["แถบแท็ก","แท็กยอดนิยมเหนือรายการ"], cb_tips: ["ทิปในแชต","ข้อความเกี่ยวกับโทเคนและทิป"], chat_boost: ["บูสต์ในแชต","การ์ด “บูสต์โมเดล N วินาที”"], chat_tips: ["ทิปในแชต","ข้อความ “ให้โทเคน” เมนูทิป"], chat_topic: ["หัวข้อห้อง","บล็อกยาว “หัวข้อ: …” ตอนต้นแชต"], chathdr: ["แถบ “แชตกับ …”","แถบ “แชตฟรีกับ …” และราชาทิป"], ftr: ["ท้ายหน้า","ส่วนท้าย โซเชียล ข้อความ SEO"], games: ["ปุ่ม “เกม”","ไอคอนจอยเกมในแถบสีแดง"], hdr_d: ["ส่วนหัวเว็บไซต์ (PC)","แถบด้านบนบนคอมพิวเตอร์"], hdr_m: ["ส่วนหัวเว็บไซต์","โลโก้ เมนู ปุ่มเข้าสู่ระบบ"], joinpop: ["หน้าต่างสมัครสมาชิก","ป๊อปอัป “สร้างบัญชี”"], lastview: ["แจ้งเตือน “ดูต่อ”","ปุ่มบนตัวอย่างของโมเดลที่ดูล่าสุด"], modelchat: ["แชตป๊อปอัปของโมเดล","หน้าต่างสีชมพูพร้อมคำทักทายอัตโนมัติด้านล่าง"], popups: ["หน้าต่างป๊อปอัป","เลือกแชต กลุ่ม คนแอบดู"], promo: ["โฆษณาและแบนเนอร์","แบนเนอร์ โปรโมชัน แถบ “เข้าร่วม” บนเครื่องเล่น แอป"], sc_about: ["บล็อกใต้เครื่องเล่น","เกี่ยวกับฉัน ตารางเวลา โมเดลที่คล้ายกัน"], sc_cookie: ["แจ้งเตือนคุกกี้","การแจ้งเตือนคุกกี้ด้านล่าง"], sc_footer: ["ท้ายหน้า","ส่วนท้ายและข้อความทางกฎหมาย"], sc_goal: ["เป้าหมาย (goal)","แถบความคืบหน้าของเป้าหมาย"], sc_hdr: ["ส่วนหัวเว็บไซต์","ส่วนหัวที่มีโลโก้ ค้นหา และปุ่มเข้าสู่ระบบ"], sc_king: ["ราชาทิป","แถบราชาทิป"], sc_promo: ["โฆษณาและแบนเนอร์","จับรางวัลโทเคน ปุ่ม “เข้าร่วม” บนเครื่องเล่น"], sc_sub: ["แถบโมเดล","อวาตาร์ ติดตาม และเมนูเหนือเครื่องเล่น"], sc_topic: ["หัวข้อห้อง","ข้อความหัวข้อเหนือและในเครื่องเล่น"], sc_widgets: ["วิดเจ็ตบนเครื่องเล่น","โพลและรีแอคชันเหนือวิดีโอ"], sc_wm: ["ลายน้ำ","โลโก้เหนือวิดีโอ"], seotitle: ["หัวข้อโฆษณา","หัวข้อ SEO เหนือรายการ"], tabs: ["แท็บและการเรียง","สาวๆ/คู่รัก ตัวกรอง ไอคอนเรียง"], tipking: ["ราชาทิป","แถบราชาและหน้าต่างทิป"], tips: ["คำแนะนำในแชต","ทูลทิปและการแจ้งเตือน"], wm: ["ลายน้ำ (PC)","โลโก้เหนือวิดีโอ"], }, }; const SUPPORT_TXT = { RU: { or: "Chaturbate · реферальная ссылка", sub: "Создать аккаунт и поддержать автора скрипта", btn: "Открыть", note: "Это реферальная ссылка автора скрипта. Если вы зарегистрируетесь на Chaturbate по ней, автор получит вознаграждение от Chaturbate. Для вас это бесплатно: цена и условия не меняются. Ссылка откроется в новой вкладке только после вашего нажатия." }, EN: { or: "Chaturbate · referral link", sub: "Create an account and support the script author", btn: "Open", note: "This is the script author's referral link. If you sign up on Chaturbate through it, the author receives a reward from Chaturbate. It's free for you: the price and terms don't change. The link opens in a new tab only after you click." }, UK: { or: "Chaturbate · реферальне посилання", sub: "Створити акаунт і підтримати автора скрипта", btn: "Відкрити", note: "Це реферальне посилання автора скрипта. Якщо ви зареєструєтеся на Chaturbate за ним, автор отримає винагороду від Chaturbate. Для вас це безкоштовно: ціна й умови не змінюються. Посилання відкриється в новій вкладці лише після вашого натискання." }, ES: { or: "Chaturbate · enlace de referido", sub: "Crear una cuenta y apoyar al autor del script", btn: "Abrir", note: "Este es el enlace de referido del autor del script. Si te registras en Chaturbate con él, el autor recibe una recompensa de Chaturbate. Para ti es gratis: el precio y las condiciones no cambian. El enlace se abre en una pestaña nueva solo cuando lo pulsas." }, DE: { or: "Chaturbate · Empfehlungslink", sub: "Konto erstellen und den Autor des Skripts unterstützen", btn: "Öffnen", note: "Das ist der Empfehlungslink des Skript-Autors. Wenn du dich über ihn bei Chaturbate registrierst, erhält der Autor eine Vergütung von Chaturbate. Für dich ist das kostenlos: Preis und Bedingungen ändern sich nicht. Der Link öffnet sich nur nach deinem Klick in einem neuen Tab." }, FR: { or: "Chaturbate · lien de parrainage", sub: "Créer un compte et soutenir l’auteur du script", btn: "Ouvrir", note: "Ceci est le lien de parrainage de l’auteur du script. Si vous vous inscrivez sur Chaturbate via ce lien, l’auteur reçoit une rémunération de Chaturbate. C’est gratuit pour vous : le prix et les conditions ne changent pas. Le lien s’ouvre dans un nouvel onglet uniquement après votre clic." }, PT: { or: "Chaturbate · link de indicação", sub: "Criar uma conta e apoiar o autor do script", btn: "Abrir", note: "Este é o link de indicação do autor do script. Se você se cadastrar no Chaturbate por ele, o autor recebe uma recompensa do Chaturbate. Para você é grátis: o preço e as condições não mudam. O link só abre em uma nova aba depois do seu clique." }, IT: { or: "Chaturbate · link di affiliazione", sub: "Crea un account e sostieni l’autore dello script", btn: "Apri", note: "Questo è il link di affiliazione dell’autore dello script. Se ti registri su Chaturbate tramite questo link, l’autore riceve un compenso da Chaturbate. Per te è gratis: prezzo e condizioni non cambiano. Il link si apre in una nuova scheda solo dopo il tuo clic." }, PL: { or: "Chaturbate · link polecający", sub: "Załóż konto i wesprzyj autora skryptu", btn: "Otwórz", note: "To link polecający autora skryptu. Jeśli zarejestrujesz się na Chaturbate przez ten link, autor otrzyma wynagrodzenie od Chaturbate. Dla ciebie jest to bezpłatne: cena i warunki się nie zmieniają. Link otworzy się w nowej karcie dopiero po kliknięciu." }, TR: { or: "Chaturbate · referans bağlantısı", sub: "Hesap oluştur ve betiğin yazarını destekle", btn: "Aç", note: "Bu, betiğin yazarının referans bağlantısıdır. Bu bağlantı üzerinden Chaturbate’e kayıt olursanız, yazar Chaturbate’ten bir ödül alır. Sizin için ücretsizdir: fiyat ve koşullar değişmez. Bağlantı yalnızca tıkladığınızda yeni sekmede açılır." }, NL: { or: "Chaturbate · uitnodigingslink", sub: "Maak een account aan en steun de auteur van het script", btn: "Openen", note: "Dit is de uitnodigingslink van de scriptauteur. Als je je via deze link registreert op Chaturbate, ontvangt de auteur een vergoeding van Chaturbate. Voor jou is het gratis: prijs en voorwaarden veranderen niet. De link opent pas na jouw klik in een nieuw tabblad." }, CS: { or: "Chaturbate · odkaz s doporučením", sub: "Vytvořit účet a podpořit autora skriptu", btn: "Otevřít", note: "Toto je odkaz autora skriptu s doporučením. Pokud se přes něj zaregistrujete na Chaturbate, autor získá odměnu od Chaturbate. Pro vás je to zdarma: cena ani podmínky se nemění. Odkaz se otevře v nové záložce až po kliknutí." }, RO: { or: "Chaturbate · link de recomandare", sub: "Creează un cont și susține autorul scriptului", btn: "Deschide", note: "Acesta este linkul de recomandare al autorului scriptului. Dacă te înregistrezi pe Chaturbate prin el, autorul primește o recompensă de la Chaturbate. Pentru tine este gratuit: prețul și condițiile nu se schimbă. Linkul se deschide într-o filă nouă doar după ce dai clic." }, HU: { or: "Chaturbate · meghívólink", sub: "Fiók létrehozása és a szkript szerzőjének támogatása", btn: "Megnyitás", note: "Ez a szkript szerzőjének meghívólinkje. Ha ezen keresztül regisztrálsz a Chaturbate-en, a szerző jutalmat kap a Chaturbate-től. Neked ingyenes: az ár és a feltételek nem változnak. A link csak a kattintásod után nyílik meg új lapon." }, BG: { or: "Chaturbate · реферална връзка", sub: "Създайте акаунт и подкрепете автора на скрипта", btn: "Отвори", note: "Това е препоръчителната връзка на автора на скрипта. Ако се регистрирате в Chaturbate през нея, авторът получава възнаграждение от Chaturbate. За вас това е безплатно: цената и условията не се променят. Връзката се отваря в нов раздел само след като натиснете." }, EL: { or: "Chaturbate · σύνδεσμος παραπομπής", sub: "Δημιουργία λογαριασμού και υποστήριξη του δημιουργού του script", btn: "Άνοιγμα", note: "Αυτός είναι ο σύνδεσμος παραπομπής του δημιουργού του script. Αν εγγραφείτε στο Chaturbate μέσω αυτού, ο δημιουργός λαμβάνει αμοιβή από το Chaturbate. Για εσάς είναι δωρεάν: η τιμή και οι όροι δεν αλλάζουν. Ο σύνδεσμος ανοίγει σε νέα καρτέλα μόνο μετά το κλικ σας." }, JA: { or: "Chaturbate · 紹介リンク", sub: "アカウントを作成して、スクリプトの作者を支援", btn: "開く", note: "これはスクリプト作者の紹介リンクです。このリンクからChaturbateに登録すると、作者はChaturbateから報酬を受け取ります。あなたは無料で、料金や条件は変わりません。リンクはクリックしたときだけ新しいタブで開きます。" }, ZH: { or: "Chaturbate · 推荐链接", sub: "创建账号并支持脚本作者", btn: "打开", note: "这是脚本作者的推荐链接。如果你通过它注册 Chaturbate,作者会从 Chaturbate 获得奖励。对你来说完全免费,价格和条款不会改变。链接只会在你点击后于新标签页打开。" }, KO: { or: "Chaturbate · 추천 링크", sub: "계정을 만들고 스크립트 제작자를 후원하기", btn: "열기", note: "스크립트 작성자의 추천 링크입니다. 이 링크로 Chaturbate에 가입하면 작성자가 Chaturbate로부터 보상을 받습니다. 사용자에게는 무료이며 가격과 조건은 달라지지 않습니다. 링크는 클릭한 경우에만 새 탭에서 열립니다." }, HE: { or: "Chaturbate · קישור הפניה", sub: "יצירת חשבון ותמיכה ביוצר הסקריפט", btn: "פתח", note: "זה קישור ההפניה של יוצר הסקריפט. אם תירשמו ל-Chaturbate דרכו, היוצר יקבל תגמול מ-Chaturbate. עבורכם זה בחינם: המחיר והתנאים לא משתנים. הקישור נפתח בלשונית חדשה רק לאחר הלחיצה שלכם." }, AR: { or: "Chaturbate · رابط الإحالة", sub: "أنشئ حسابًا وادعم مطوّر السكربت", btn: "فتح", note: "هذا رابط الإحالة الخاص بمطوّر السكربت. إذا سجّلت في Chaturbate عبره، يحصل المطوّر على مكافأة من Chaturbate. هذا مجاني بالنسبة لك: السعر والشروط لا يتغيران. يُفتح الرابط في علامة تبويب جديدة فقط بعد نقرك عليه." }, HI: { or: "Chaturbate · रेफ़रल लिंक", sub: "खाता बनाएँ और स्क्रिप्ट के लेखक का समर्थन करें", btn: "खोलें", note: "यह स्क्रिप्ट के लेखक का रेफ़रल लिंक है। अगर आप इसके ज़रिए Chaturbate पर रजिस्टर करते हैं, तो लेखक को Chaturbate से पुरस्कार मिलता है। आपके लिए यह मुफ़्त है: कीमत और शर्तें नहीं बदलतीं। लिंक आपके क्लिक करने के बाद ही नए टैब में खुलता है।" }, ID: { or: "Chaturbate · tautan referral", sub: "Buat akun dan dukung pembuat skrip", btn: "Buka", note: "Ini adalah tautan referral pembuat skrip. Jika kamu mendaftar di Chaturbate lewat tautan ini, pembuat menerima imbalan dari Chaturbate. Untukmu ini gratis: harga dan ketentuan tidak berubah. Tautan hanya terbuka di tab baru setelah kamu mengkliknya." }, VI: { or: "Chaturbate · liên kết giới thiệu", sub: "Tạo tài khoản và ủng hộ tác giả của script", btn: "Mở", note: "Đây là liên kết giới thiệu của tác giả tập lệnh. Nếu bạn đăng ký Chaturbate qua liên kết này, tác giả sẽ nhận phần thưởng từ Chaturbate. Với bạn thì hoàn toàn miễn phí: giá và điều khoản không thay đổi. Liên kết chỉ mở trong tab mới sau khi bạn nhấn." }, TH: { or: "Chaturbate · ลิงก์แนะนำ", sub: "สร้างบัญชีและสนับสนุนผู้สร้างสคริปต์", btn: "เปิด", note: "นี่คือลิงก์แนะนำของผู้สร้างสคริปต์ หากคุณสมัคร Chaturbate ผ่านลิงก์นี้ ผู้สร้างจะได้รับรางวัลจาก Chaturbate สำหรับคุณไม่มีค่าใช้จ่าย ราคาและเงื่อนไขไม่เปลี่ยนแปลง ลิงก์จะเปิดในแท็บใหม่ก็ต่อเมื่อคุณกดเท่านั้น" } }; const REC_FMT_T = { EN: ['Format', 'MP4 is not supported by this browser, WebM will be used'], RU: ['Формат', 'Этот браузер не поддерживает MP4, будет использован WebM'], UK: ['Формат', 'Цей браузер не підтримує MP4, буде використано WebM'], ES: ['Formato', 'Este navegador no admite MP4, se usará WebM'], DE: ['Format', 'MP4 wird von diesem Browser nicht unterstützt, es wird WebM verwendet'], FR: ['Format', 'MP4 n’est pas pris en charge par ce navigateur, WebM sera utilisé'], PT: ['Formato', 'Este navegador não suporta MP4, será usado WebM'], IT: ['Formato', 'MP4 non è supportato da questo browser, verrà usato WebM'], PL: ['Format', 'Ta przeglądarka nie obsługuje MP4, zostanie użyty WebM'], TR: ['Biçim', 'Bu tarayıcı MP4 desteklemiyor, WebM kullanılacak'], NL: ['Formaat', 'Deze browser ondersteunt MP4 niet, WebM wordt gebruikt'], CS: ['Formát', 'Tento prohlížeč nepodporuje MP4, bude použit WebM'], RO: ['Format', 'Acest browser nu acceptă MP4, se va folosi WebM'], HU: ['Formátum', 'Ez a böngésző nem támogatja az MP4-et, WebM lesz használva'], BG: ['Формат', 'Този браузър не поддържа MP4, ще се използва WebM'], EL: ['Μορφή', 'Αυτό το πρόγραμμα περιήγησης δεν υποστηρίζει MP4, θα χρησιμοποιηθεί WebM'], JA: ['形式', 'このブラウザはMP4に対応していないため、WebMが使用されます'], ZH: ['格式', '此浏览器不支持 MP4,将使用 WebM'], KO: ['형식', '이 브라우저는 MP4를 지원하지 않아 WebM이 사용됩니다'], HE: ['פורמט', 'הדפדפן הזה אינו תומך ב-MP4, ייעשה שימוש ב-WebM'], AR: ['الصيغة', 'هذا المتصفح لا يدعم MP4، وسيتم استخدام WebM'], HI: ['फ़ॉर्मैट', 'यह ब्राउज़र MP4 का समर्थन नहीं करता, WebM का उपयोग किया जाएगा'], ID: ['Format', 'Browser ini tidak mendukung MP4, WebM akan digunakan'], VI: ['Định dạng', 'Trình duyệt này không hỗ trợ MP4, sẽ dùng WebM'], TH: ['รูปแบบ', 'เบราว์เซอร์นี้ไม่รองรับ MP4 จะใช้ WebM แทน'] }; const PD_T = { RU: { title: "Отчёт парсера", copy: "Копировать", copied: "Скопировано", reset: "Сбросить", nodata: "Данных пока нет: отчёт заполняется по мере обновления закладок.", ok: "Всё в порядке: без статуса {u} из {n}.", warn: "У {u} из {n} моделей статус не определён (порог {p}%). Скорее всего, сайт изменил ответ. Нажмите «{copy}» и пришлите JSON, чтобы поправить таблицу ключей BC_SCHEMA.", age: "возраст", unknown: "неизвестно", diag: "Диагностика", nostatus: "без статуса", sumT: "Сводка", models: "Модели", dash: "«—» оранжевым: у онлайн-модели поле пустое.", toast: "Парсер {site}: у {u} из {n} моделей не определился статус. Возможно, сайт изменил разметку. Нажмите, чтобы открыть отчёт.", }, EN: { title: "Parser report", copy: "Copy", copied: "Copied", reset: "Reset", nodata: "No data yet: the report fills in as bookmarks refresh.", ok: "All good: {u} of {n} without status.", warn: "Status is unknown for {u} of {n} models (threshold {p}%). The site likely changed its response. Press \"{copy}\" and send the JSON to adjust the BC_SCHEMA key table.", age: "age", unknown: "unknown", diag: "Diagnostics", nostatus: "no status", sumT: "Summary", models: "Models", dash: "Orange \"—\": an online model has this field empty.", toast: "{site} parser: status is unknown for {u} of {n} models. The site markup may have changed. Click to open the report.", }, UK: { title: "Звіт парсера", copy: "Копіювати", copied: "Скопійовано", reset: "Скинути", nodata: "Даних поки немає: звіт заповнюється в міру оновлення закладок.", ok: "Усе гаразд: без статусу {u} із {n}.", warn: "У {u} із {n} моделей статус не визначено (поріг {p}%). Найімовірніше, сайт змінив відповідь. Натисніть «{copy}» і надішліть JSON, щоб виправити таблицю ключів BC_SCHEMA.", age: "вік", unknown: "невідомо", diag: "Діагностика", nostatus: "без статусу", toast: "Парсер {site}: у {u} із {n} моделей не визначився статус. Можливо, сайт змінив розмітку. Натисніть, щоб відкрити звіт.", }, ES: { title: "Informe del analizador", copy: "Copiar", copied: "Copiado", reset: "Restablecer", nodata: "Aún no hay datos: el informe se rellena a medida que se actualizan los favoritos.", ok: "Todo en orden: {u} de {n} sin estado.", warn: "El estado es desconocido en {u} de {n} modelos (umbral {p}%). Probablemente el sitio cambió su respuesta. Pulsa «{copy}» y envía el JSON para ajustar la tabla de claves BC_SCHEMA.", age: "antigüedad", unknown: "desconocido", diag: "Diagnóstico", nostatus: "sin estado", toast: "Analizador {site}: estado desconocido en {u} de {n} modelos. Puede que el sitio haya cambiado su estructura. Haz clic para abrir el informe.", }, DE: { title: "Parser-Bericht", copy: "Kopieren", copied: "Kopiert", reset: "Zurücksetzen", nodata: "Noch keine Daten: Der Bericht füllt sich beim Aktualisieren der Favoriten.", ok: "Alles in Ordnung: {u} von {n} ohne Status.", warn: "Bei {u} von {n} Models ist der Status unbekannt (Schwelle {p} %). Wahrscheinlich hat die Seite ihre Antwort geändert. Klicke auf „{copy}“ und sende das JSON, um die Schlüsseltabelle BC_SCHEMA anzupassen.", age: "Alter", unknown: "unbekannt", diag: "Diagnose", nostatus: "ohne Status", toast: "{site}-Parser: Bei {u} von {n} Models ist der Status unbekannt. Möglicherweise hat sich das Seitenlayout geändert. Klicken, um den Bericht zu öffnen.", }, FR: { title: "Rapport de l’analyseur", copy: "Copier", copied: "Copié", reset: "Réinitialiser", nodata: "Pas encore de données : le rapport se remplit à mesure que les favoris sont actualisés.", ok: "Tout va bien : {u} sur {n} sans statut.", warn: "Le statut est inconnu pour {u} modèles sur {n} (seuil {p} %). Le site a probablement modifié sa réponse. Cliquez sur « {copy} » et envoyez le JSON pour ajuster la table de clés BC_SCHEMA.", age: "âge", unknown: "inconnu", diag: "Diagnostic", nostatus: "sans statut", toast: "Analyseur {site} : statut inconnu pour {u} modèles sur {n}. La structure du site a peut-être changé. Cliquez pour ouvrir le rapport.", }, PT: { title: "Relatório do analisador", copy: "Copiar", copied: "Copiado", reset: "Redefinir", nodata: "Ainda sem dados: o relatório é preenchido conforme os favoritos são atualizados.", ok: "Tudo certo: {u} de {n} sem status.", warn: "O status é desconhecido em {u} de {n} modelos (limite {p}%). Provavelmente o site mudou a resposta. Clique em “{copy}” e envie o JSON para ajustar a tabela de chaves BC_SCHEMA.", age: "idade", unknown: "desconhecido", diag: "Diagnóstico", nostatus: "sem status", toast: "Analisador {site}: status desconhecido em {u} de {n} modelos. A estrutura do site pode ter mudado. Clique para abrir o relatório.", }, IT: { title: "Report del parser", copy: "Copia", copied: "Copiato", reset: "Reimposta", nodata: "Ancora nessun dato: il report si compila man mano che i preferiti si aggiornano.", ok: "Tutto a posto: {u} su {n} senza stato.", warn: "Lo stato è sconosciuto per {u} profili su {n} (soglia {p}%). Probabilmente il sito ha cambiato la risposta. Premi «{copy}» e invia il JSON per correggere la tabella di chiavi BC_SCHEMA.", age: "età", unknown: "sconosciuto", diag: "Diagnostica", nostatus: "senza stato", toast: "Parser {site}: stato sconosciuto per {u} profili su {n}. Il sito potrebbe aver cambiato struttura. Clicca per aprire il report.", }, PL: { title: "Raport parsera", copy: "Kopiuj", copied: "Skopiowano", reset: "Resetuj", nodata: "Brak danych: raport wypełnia się w miarę odświeżania zakładek.", ok: "Wszystko w porządku: {u} z {n} bez statusu.", warn: "Status jest nieznany dla {u} z {n} profili (próg {p}%). Prawdopodobnie serwis zmienił odpowiedź. Kliknij „{copy}” i wyślij JSON, aby poprawić tabelę kluczy BC_SCHEMA.", age: "wiek", unknown: "nieznany", diag: "Diagnostyka", nostatus: "bez statusu", toast: "Parser {site}: status nieznany dla {u} z {n} profili. Układ strony mógł się zmienić. Kliknij, aby otworzyć raport.", }, TR: { title: "Ayrıştırıcı raporu", copy: "Kopyala", copied: "Kopyalandı", reset: "Sıfırla", nodata: "Henüz veri yok: rapor, yer imleri yenilendikçe dolar.", ok: "Her şey yolunda: {n} modelden {u} tanesinin durumu yok.", warn: "{n} modelden {u} tanesinin durumu bilinmiyor (eşik %{p}). Site büyük olasılıkla yanıtını değiştirdi. “{copy}” düğmesine basıp JSON’u gönderin; BC_SCHEMA anahtar tablosu düzeltilsin.", age: "yaş", unknown: "bilinmiyor", diag: "Tanılama", nostatus: "durum yok", toast: "{site} ayrıştırıcı: {n} modelden {u} tanesinin durumu bilinmiyor. Sitenin yapısı değişmiş olabilir. Raporu açmak için tıklayın.", }, NL: { title: "Parserrapport", copy: "Kopiëren", copied: "Gekopieerd", reset: "Resetten", nodata: "Nog geen gegevens: het rapport vult zich naarmate favorieten worden vernieuwd.", ok: "Alles in orde: {u} van {n} zonder status.", warn: "De status is onbekend bij {u} van {n} modellen (drempel {p}%). De site heeft waarschijnlijk zijn respons gewijzigd. Klik op “{copy}” en stuur de JSON om de BC_SCHEMA-sleuteltabel aan te passen.", age: "leeftijd", unknown: "onbekend", diag: "Diagnostiek", nostatus: "geen status", toast: "{site}-parser: status onbekend bij {u} van {n} modellen. De opmaak van de site is mogelijk gewijzigd. Klik om het rapport te openen.", }, CS: { title: "Zpráva parseru", copy: "Kopírovat", copied: "Zkopírováno", reset: "Obnovit", nodata: "Zatím žádná data: zpráva se plní s aktualizací záložek.", ok: "Vše v pořádku: {u} z {n} bez stavu.", warn: "U {u} z {n} modelů není stav známý (práh {p} %). Web pravděpodobně změnil odpověď. Klikněte na „{copy}“ a pošlete JSON, aby šla upravit tabulka klíčů BC_SCHEMA.", age: "stáří", unknown: "neznámý", diag: "Diagnostika", nostatus: "bez stavu", toast: "Parser {site}: u {u} z {n} modelů není stav známý. Web možná změnil strukturu. Kliknutím otevřete zprávu.", }, RO: { title: "Raport parser", copy: "Copiază", copied: "Copiat", reset: "Resetează", nodata: "Încă nu există date: raportul se completează pe măsură ce favoritele se actualizează.", ok: "Totul e în regulă: {u} din {n} fără stare.", warn: "Starea este necunoscută pentru {u} din {n} modele (prag {p}%). Probabil site-ul și-a schimbat răspunsul. Apasă „{copy}” și trimite JSON-ul pentru a ajusta tabelul de chei BC_SCHEMA.", age: "vârstă", unknown: "necunoscut", diag: "Diagnostic", nostatus: "fără stare", toast: "Parser {site}: stare necunoscută pentru {u} din {n} modele. Structura site-ului s-ar putea să se fi schimbat. Dă clic pentru a deschide raportul.", }, HU: { title: "Elemző jelentés", copy: "Másolás", copied: "Másolva", reset: "Visszaállítás", nodata: "Még nincs adat: a jelentés a könyvjelzők frissítésével töltődik.", ok: "Minden rendben: {n} modellből {u} állapot nélküli.", warn: "{n} modellből {u} esetében ismeretlen az állapot (küszöb: {p}%). A webhely valószínűleg megváltoztatta a válaszát. Kattints a „{copy}” gombra, és küldd el a JSON-t a BC_SCHEMA kulcstábla javításához.", age: "kor", unknown: "ismeretlen", diag: "Diagnosztika", nostatus: "állapot nélkül", toast: "{site} elemző: {n} modellből {u} esetében ismeretlen az állapot. Lehet, hogy megváltozott az oldal szerkezete. Kattints a jelentés megnyitásához.", }, BG: { title: "Отчет на парсера", copy: "Копирай", copied: "Копирано", reset: "Нулирай", nodata: "Все още няма данни: отчетът се попълва при обновяване на любимите.", ok: "Всичко е наред: {u} от {n} без статус.", warn: "Статусът е неизвестен за {u} от {n} модела (праг {p}%). Най-вероятно сайтът е променил отговора си. Натиснете „{copy}“ и изпратете JSON, за да се поправи таблицата с ключове BC_SCHEMA.", age: "възраст", unknown: "неизвестно", diag: "Диагностика", nostatus: "без статус", toast: "Парсер {site}: статусът е неизвестен за {u} от {n} модела. Възможно е сайтът да е променил разметката. Кликнете, за да отворите отчета.", }, EL: { title: "Αναφορά parser", copy: "Αντιγραφή", copied: "Αντιγράφηκε", reset: "Επαναφορά", nodata: "Δεν υπάρχουν δεδομένα ακόμη: η αναφορά συμπληρώνεται καθώς ανανεώνονται τα αγαπημένα.", ok: "Όλα καλά: {u} από {n} χωρίς κατάσταση.", warn: "Η κατάσταση είναι άγνωστη για {u} από {n} μοντέλα (όριο {p}%). Πιθανότατα ο ιστότοπος άλλαξε την απάντησή του. Πατήστε «{copy}» και στείλτε το JSON για να διορθωθεί ο πίνακας κλειδιών BC_SCHEMA.", age: "ηλικία", unknown: "άγνωστη", diag: "Διάγνωση", nostatus: "χωρίς κατάσταση", toast: "Parser {site}: άγνωστη κατάσταση για {u} από {n} μοντέλα. Η δομή του ιστότοπου μπορεί να άλλαξε. Κάντε κλικ για να ανοίξει η αναφορά.", }, JA: { title: "パーサーレポート", copy: "コピー", copied: "コピーしました", reset: "リセット", nodata: "まだデータがありません。お気に入りの更新に合わせてレポートが埋まります。", ok: "問題ありません: {n}件中{u}件がステータスなし。", warn: "{n}件中{u}件のモデルでステータス不明です(しきい値 {p}%)。サイト側の応答が変わった可能性があります。「{copy}」を押して JSON を送ってください。BC_SCHEMA のキー表を修正します。", age: "経過", unknown: "不明", diag: "診断", nostatus: "ステータスなし", toast: "{site} パーサー: {n}件中{u}件のモデルでステータスが不明です。サイトの構造が変わった可能性があります。クリックでレポートを開きます。", }, ZH: { title: "解析器报告", copy: "复制", copied: "已复制", reset: "重置", nodata: "暂无数据:报告会随收藏更新逐步填充。", ok: "一切正常:{n} 个中有 {u} 个无状态。", warn: "{n} 个模特中有 {u} 个状态未知(阈值 {p}%)。网站很可能更改了响应。请点击“{copy}”并发送 JSON,以便调整 BC_SCHEMA 键表。", age: "时长", unknown: "未知", diag: "诊断", nostatus: "无状态", toast: "{site} 解析器:{n} 个模特中有 {u} 个状态未知。网站结构可能已更改。点击打开报告。", }, KO: { title: "파서 보고서", copy: "복사", copied: "복사됨", reset: "초기화", nodata: "아직 데이터가 없습니다: 즐겨찾기가 갱신되면서 보고서가 채워집니다.", ok: "정상: {n}개 중 {u}개 상태 없음.", warn: "{n}개 모델 중 {u}개의 상태를 알 수 없습니다(임계값 {p}%). 사이트가 응답을 변경했을 가능성이 큽니다. \"{copy}\"를 눌러 JSON을 보내 주시면 BC_SCHEMA 키 표를 수정할 수 있습니다.", age: "경과", unknown: "알 수 없음", diag: "진단", nostatus: "상태 없음", toast: "{site} 파서: {n}개 모델 중 {u}개의 상태를 알 수 없습니다. 사이트 구조가 바뀌었을 수 있습니다. 클릭하면 보고서를 엽니다.", }, HE: { title: "דוח מנתח", copy: "העתק", copied: "הועתק", reset: "איפוס", nodata: "אין עדיין נתונים: הדוח מתמלא עם רענון המועדפים.", ok: "הכול תקין: {u} מתוך {n} ללא סטטוס.", warn: "הסטטוס לא ידוע עבור {u} מתוך {n} מודלים (סף {p}%). כנראה האתר שינה את התשובה. לחצו על \"{copy}\" ושלחו את ה-JSON כדי לתקן את טבלת המפתחות BC_SCHEMA.", age: "גיל", unknown: "לא ידוע", diag: "אבחון", nostatus: "ללא סטטוס", toast: "מנתח {site}: הסטטוס לא ידוע עבור {u} מתוך {n} מודלים. ייתכן שמבנה האתר השתנה. לחצו כדי לפתוח את הדוח.", }, AR: { title: "تقرير المحلّل", copy: "نسخ", copied: "تم النسخ", reset: "إعادة تعيين", nodata: "لا توجد بيانات بعد: يمتلئ التقرير مع تحديث المفضلة.", ok: "كل شيء على ما يرام: {u} من {n} بلا حالة.", warn: "الحالة غير معروفة لدى {u} من {n} نماذج (الحد {p}%). على الأرجح غيّر الموقع استجابته. اضغط «{copy}» وأرسل JSON لتعديل جدول المفاتيح BC_SCHEMA.", age: "العمر", unknown: "غير معروف", diag: "التشخيص", nostatus: "بلا حالة", toast: "محلّل {site}: الحالة غير معروفة لدى {u} من {n} نماذج. ربما تغيّرت بنية الموقع. انقر لفتح التقرير.", }, HI: { title: "पार्सर रिपोर्ट", copy: "कॉपी करें", copied: "कॉपी हो गया", reset: "रीसेट करें", nodata: "अभी कोई डेटा नहीं: पसंदीदा रिफ्रेश होने पर रिपोर्ट भरती जाती है।", ok: "सब ठीक है: {n} में से {u} बिना स्थिति के।", warn: "{n} में से {u} मॉडल की स्थिति अज्ञात है (सीमा {p}%)। संभवतः साइट ने अपना उत्तर बदल दिया है। \"{copy}\" दबाएँ और BC_SCHEMA कुंजी तालिका ठीक करने के लिए JSON भेजें।", age: "आयु", unknown: "अज्ञात", diag: "निदान", nostatus: "बिना स्थिति", toast: "{site} पार्सर: {n} में से {u} मॉडल की स्थिति अज्ञात है। हो सकता है साइट की संरचना बदल गई हो। रिपोर्ट खोलने के लिए क्लिक करें।", }, ID: { title: "Laporan parser", copy: "Salin", copied: "Tersalin", reset: "Atur ulang", nodata: "Belum ada data: laporan terisi seiring favorit diperbarui.", ok: "Semua baik: {u} dari {n} tanpa status.", warn: "Status tidak diketahui pada {u} dari {n} model (ambang {p}%). Kemungkinan situs mengubah responsnya. Tekan \"{copy}\" lalu kirim JSON untuk menyesuaikan tabel kunci BC_SCHEMA.", age: "usia", unknown: "tidak diketahui", diag: "Diagnostik", nostatus: "tanpa status", toast: "Parser {site}: status tidak diketahui pada {u} dari {n} model. Struktur situs mungkin berubah. Klik untuk membuka laporan.", }, VI: { title: "Báo cáo trình phân tích", copy: "Sao chép", copied: "Đã sao chép", reset: "Đặt lại", nodata: "Chưa có dữ liệu: báo cáo được điền dần khi mục yêu thích được làm mới.", ok: "Mọi thứ ổn: {u}/{n} không có trạng thái.", warn: "Không xác định được trạng thái của {u}/{n} model (ngưỡng {p}%). Nhiều khả năng trang đã đổi phản hồi. Nhấn “{copy}” và gửi JSON để chỉnh bảng khóa BC_SCHEMA.", age: "tuổi", unknown: "không rõ", diag: "Chẩn đoán", nostatus: "không có trạng thái", toast: "Trình phân tích {site}: không xác định được trạng thái của {u}/{n} model. Cấu trúc trang có thể đã thay đổi. Nhấp để mở báo cáo.", }, TH: { title: "รายงานตัวแยกวิเคราะห์", copy: "คัดลอก", copied: "คัดลอกแล้ว", reset: "รีเซ็ต", nodata: "ยังไม่มีข้อมูล: รายงานจะเติมเมื่อรายการโปรดรีเฟรช", ok: "ปกติดี: {u} จาก {n} ไม่มีสถานะ", warn: "ไม่ทราบสถานะของ {u} จาก {n} โมเดล (เกณฑ์ {p}%) เป็นไปได้ว่าเว็บไซต์เปลี่ยนการตอบกลับ กด \"{copy}\" แล้วส่ง JSON เพื่อปรับตารางคีย์ BC_SCHEMA", age: "อายุ", unknown: "ไม่ทราบ", diag: "การวินิจฉัย", nostatus: "ไม่มีสถานะ", toast: "ตัวแยกวิเคราะห์ {site}: ไม่ทราบสถานะของ {u} จาก {n} โมเดล โครงสร้างเว็บไซต์อาจเปลี่ยนไป คลิกเพื่อเปิดรายงาน", }, }; const init = () => { if (SITE === 'cb') { try { cbHoverPreview(); } catch (err) { console.error('[cb-hover-preview]', err); } } try { new App(); } catch (err) { console.error('[cams-unchained] App init failed', err); } }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); })();